[!!!][TASK] Drop additional palette handling
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / Container / PaletteAndSingleContainer.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form\Container;
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
17 use TYPO3\CMS\Core\Utility\GeneralUtility;
18 use TYPO3\CMS\Lang\LanguageService;
19 use TYPO3\CMS\Backend\Utility\IconUtility;
20 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
21 use TYPO3\CMS\Backend\Utility\BackendUtility;
22 use TYPO3\CMS\Backend\Form\NodeFactory;
23
24 /**
25 * Handle palettes and single fields.
26 *
27 * This container is called by TabsContainer, NoTabsContainer and ListOfFieldsContainer.
28 *
29 * This container mostly operates on TCA showItem of a specific type - the value is
30 * coming in from upper containers as "fieldArray". It handles palettes with all its
31 * different options and prepares rendering of single fields for the SingleFieldContainer.
32 */
33 class PaletteAndSingleContainer extends AbstractContainer {
34
35 /**
36 * Final result array accumulating results from children and final HTML
37 *
38 * @var array
39 */
40 protected $resultArray = array();
41
42 /**
43 * Entry method
44 *
45 * @return array As defined in initializeResultArray() of AbstractNode
46 */
47 public function render() {
48 $languageService = $this->getLanguageService();
49 $table = $this->globalOptions['table'];
50
51 /**
52 * The first code block creates a target structure array to later create the final
53 * HTML string. The single fields and sub containers are rendered here already and
54 * other parts of the return array from children except html are accumulated in
55 * $this->resultArray
56 *
57 $targetStructure = array(
58 0 => array(
59 'type' => 'palette',
60 'fieldName' => 'palette1',
61 'fieldLabel' => 'palette1',
62 'elements' => array(
63 0 => array(
64 'type' => 'single',
65 'fieldName' => 'paletteName',
66 'fieldLabel' => 'element1',
67 'fieldHtml' => 'element1',
68 ),
69 1 => array(
70 'type' => 'linebreak',
71 ),
72 2 => array(
73 'type' => 'single',
74 'fieldName' => 'paletteName',
75 'fieldLabel' => 'element2',
76 'fieldHtml' => 'element2',
77 ),
78 ),
79 ),
80 1 => array(
81 'type' => 'single',
82 'fieldName' => 'element3',
83 'fieldLabel' => 'element3',
84 'fieldHtml' => 'element3',
85 ),
86 2 => array(
87 'type' => 'palette2',
88 'fieldName' => 'palette2',
89 'fieldLabel' => '', // Palettes may not have a label
90 'elements' => array(
91 0 => array(
92 'type' => 'single',
93 'fieldName' => 'element4',
94 'fieldLabel' => 'element4',
95 'fieldHtml' => 'element4',
96 ),
97 1 => array(
98 'type' => 'linebreak',
99 ),
100 2 => array(
101 'type' => 'single',
102 'fieldName' => 'element5',
103 'fieldLabel' => 'element5',
104 'fieldHtml' => 'element5',
105 ),
106 ),
107 ),
108 );
109 */
110
111 // Create an intermediate structure of rendered sub elements and elements nested in palettes
112 $targetStructure = array();
113 $mainStructureCounter = -1;
114 $fieldsArray = $this->globalOptions['fieldsArray'];
115 $this->resultArray = $this->initializeResultArray();
116 foreach ($fieldsArray as $fieldString) {
117 $fieldConfiguration = $this->explodeSingleFieldShowItemConfiguration($fieldString);
118 $fieldName = $fieldConfiguration['fieldName'];
119 if ($fieldName === '--palette--') {
120 $paletteElementArray = $this->createPaletteContentArray($fieldConfiguration['paletteName']);
121 if (!empty($paletteElementArray)) {
122 $mainStructureCounter ++;
123 $targetStructure[$mainStructureCounter] = array(
124 'type' => 'palette',
125 'fieldName' => $fieldConfiguration['paletteName'],
126 'fieldLabel' => $languageService->sL($fieldConfiguration['fieldLabel']),
127 'elements' => $paletteElementArray,
128 );
129 }
130 } else {
131 if (!is_array($GLOBALS['TCA'][$table]['columns'][$fieldName])) {
132 continue;
133 }
134
135 $options = $this->globalOptions;
136 $options['fieldName'] = $fieldName;
137
138 $options['renderType'] = 'singleFieldContainer';
139 /** @var NodeFactory $nodeFactory */
140 $nodeFactory = $this->globalOptions['nodeFactory'];
141 $childResultArray = $nodeFactory->create($options)->render();
142
143 if (!empty($childResultArray['html'])) {
144 $mainStructureCounter ++;
145
146 $targetStructure[$mainStructureCounter] = array(
147 'type' => 'single',
148 'fieldName' => $fieldConfiguration['fieldName'],
149 'fieldLabel' => $this->getSingleFieldLabel($fieldName, $fieldConfiguration['fieldLabel']),
150 'fieldHtml' => $childResultArray['html'],
151 );
152 }
153
154 $childResultArray['html'] = '';
155 $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $childResultArray);
156 }
157 }
158
159 // Compile final content
160 $content = array();
161 foreach ($targetStructure as $element) {
162 if ($element['type'] === 'palette') {
163 $paletteName = $element['fieldName'];
164 $paletteElementsHtml = $this->renderInnerPaletteContent($element);
165
166 $isHiddenPalette = !empty($GLOBALS['TCA'][$table]['palettes'][$paletteName]['isHiddenPalette']);
167
168 $renderUnCollapseButtonWrapper = TRUE;
169 // No button if the palette is hidden
170 if ($isHiddenPalette) {
171 $renderUnCollapseButtonWrapper = FALSE;
172 }
173 // No button if palette can not collapse on ctrl level
174 if (!empty($GLOBALS['TCA'][$table]['ctrl']['canNotCollapse'])) {
175 $renderUnCollapseButtonWrapper = FALSE;
176 }
177 // No button if palette can not collapse on palette definition level
178 if (!empty($GLOBALS['TCA'][$table]['palettes'][$paletteName]['canNotCollapse'])) {
179 $renderUnCollapseButtonWrapper = FALSE;
180 }
181 // No button if palettes are not collapsed - this is the checkbox at the end of the form
182 if (!$this->globalOptions['palettesCollapsed']) {
183 $renderUnCollapseButtonWrapper = FALSE;
184 }
185
186 if ($renderUnCollapseButtonWrapper) {
187 $cssId = str_replace('.', '_', 'FORMENGINE_' . $this->globalOptions['table'] . '_' . $paletteName . '_' . $this->globalOptions['databaseRow']['uid']);
188 $paletteElementsHtml = $this->wrapPaletteWithCollapseButton($paletteElementsHtml, $cssId);
189 } else {
190 $paletteElementsHtml = '<div class="row">' . $paletteElementsHtml . '</div>';
191 }
192
193 $content[] = $this->fieldSetWrap($paletteElementsHtml, $isHiddenPalette, $element['fieldLabel']);
194 } else {
195 // Return raw HTML only in case of user element with no wrapping requested
196 if ($this->isUserNoTableWrappingField($element)) {
197 $content[] = $element['fieldHtml'];
198 } else {
199 $content[] = $this->fieldSetWrap($this->wrapSingleFieldContentWithLabelAndOuterDiv($element));
200 }
201 }
202 }
203
204 $finalResultArray = $this->resultArray;
205 $finalResultArray['html'] = implode(LF, $content);
206 return $finalResultArray;
207 }
208
209 /**
210 * Render single fields of a given palette
211 *
212 * @param string $paletteName The palette to render
213 * @return array
214 */
215 protected function createPaletteContentArray($paletteName) {
216 $table = $this->globalOptions['table'];
217 $excludeElements = $this->globalOptions['excludeElements'];
218
219 // palette needs a palette name reference, otherwise it does not make sense to try rendering of it
220 if (empty($paletteName) || empty($GLOBALS['TCA'][$table]['palettes'][$paletteName]['showitem'])) {
221 return array();
222 }
223
224 $resultStructure = array();
225 $foundRealElement = FALSE; // Set to true if not only line breaks were rendered
226 $fieldsArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['palettes'][$paletteName]['showitem'], TRUE);
227 foreach ($fieldsArray as $fieldString) {
228 $fieldArray = $this->explodeSingleFieldShowItemConfiguration($fieldString);
229 $fieldName = $fieldArray['fieldName'];
230 if ($fieldName === '--linebreak--') {
231 $resultStructure[] = array(
232 'type' => 'linebreak',
233 );
234 } else {
235 if (in_array($fieldName, $excludeElements, TRUE) || !is_array($GLOBALS['TCA'][$table]['columns'][$fieldName])) {
236 continue;
237 }
238 $options = $this->globalOptions;
239 $options['fieldName'] = $fieldName;
240
241 $options['renderType'] = 'singleFieldContainer';
242 /** @var NodeFactory $nodeFactory */
243 $nodeFactory = $this->globalOptions['nodeFactory'];
244 $singleFieldContentArray = $nodeFactory->create($options)->render();
245
246 if (!empty($singleFieldContentArray['html'])) {
247 $foundRealElement = TRUE;
248 $resultStructure[] = array(
249 'type' => 'single',
250 'fieldName' => $fieldName,
251 'fieldLabel' => $this->getSingleFieldLabel($fieldName, $fieldArray['fieldLabel']),
252 'fieldHtml' => $singleFieldContentArray['html'],
253 );
254 $singleFieldContentArray['html'] = '';
255 }
256 $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $singleFieldContentArray);
257 }
258 }
259
260 if ($foundRealElement) {
261 return $resultStructure;
262 } else {
263 return array();
264 }
265 }
266
267 /**
268 * Renders inner content of single elements of a palette and wrap it as needed
269 *
270 * @param array $elementArray Array of elements
271 * @return string Wrapped content
272 */
273 protected function renderInnerPaletteContent(array $elementArray) {
274 // Group fields
275 $groupedFields = array();
276 $row = 0;
277 $lastLineWasLinebreak = TRUE;
278 foreach ($elementArray['elements'] as $element) {
279 if ($element['type'] === 'linebreak') {
280 if (!$lastLineWasLinebreak) {
281 $row++;
282 $groupedFields[$row][] = $element;
283 $row++;
284 $lastLineWasLinebreak = TRUE;
285 }
286 } else {
287 $lastLineWasLinebreak = FALSE;
288 $groupedFields[$row][] = $element;
289 }
290 }
291
292 $result = array();
293 // Process fields
294 foreach ($groupedFields as $fields) {
295 $numberOfItems = count($fields);
296 $colWidth = (int)floor(12 / $numberOfItems);
297 // Column class calculation
298 $colClass = "col-md-12";
299 $colClear = array();
300 if ($colWidth == 6) {
301 $colClass = "col-sm-6";
302 $colClear = array(
303 2 => 'visible-sm-block visible-md-block visible-lg-block',
304 );
305 } elseif ($colWidth === 4) {
306 $colClass = "col-sm-4";
307 $colClear = array(
308 3 => 'visible-sm-block visible-md-block visible-lg-block',
309 );
310 } elseif ($colWidth === 3) {
311 $colClass = "col-sm-6 col-md-3";
312 $colClear = array(
313 2 => 'visible-sm-block',
314 4 => 'visible-md-block visible-lg-block',
315 );
316 } elseif ($colWidth <= 2) {
317 $colClass = "checkbox-column col-sm-6 col-md-3 col-lg-2";
318 $colClear = array(
319 2 => 'visible-sm-block',
320 4 => 'visible-md-block',
321 6 => 'visible-lg-block'
322 );
323 }
324
325 // Render fields
326 for ($counter = 0; $counter < $numberOfItems; $counter++) {
327 $element = $fields[$counter];
328 if ($element['type'] === 'linebreak') {
329 if ($counter !== $numberOfItems) {
330 $result[] = '<div class="clearfix"></div>';
331 }
332 } else {
333 $result[] = $this->wrapSingleFieldContentWithLabelAndOuterDiv($element, array($colClass));
334
335 // Breakpoints
336 if ($counter + 1 < $numberOfItems && !empty($colClear)) {
337 foreach ($colClear as $rowBreakAfter => $clearClass) {
338 if (($counter + 1) % $rowBreakAfter === 0) {
339 $result[] = '<div class="clearfix '. $clearClass . '"></div>';
340 }
341 }
342 }
343 }
344 }
345 }
346
347 return implode(LF, $result);
348 }
349
350 /**
351 * Add a "collapsible" button around given content
352 *
353 * @param string $elementHtml HTML of handled palette content
354 * @param string $cssId A css id to be added
355 * @return string Wrapped content
356 */
357 protected function wrapPaletteWithCollapseButton($elementHtml, $cssId) {
358 $content = array();
359 $content[] = '<p>';
360 $content[] = '<button class="btn btn-default" type="button" data-toggle="collapse" data-target="#' . $cssId . '" aria-expanded="false" aria-controls="' . $cssId . '">';
361 $content[] = IconUtility::getSpriteIcon('actions-system-options-view');
362 $content[] = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.moreOptions'));
363 $content[] = '</button>';
364 $content[] = '</p>';
365 $content[] = '<div id="' . $cssId . '" class="form-section-collapse collapse">';
366 $content[] = '<div class="row">' . $elementHtml . '</div>';
367 $content[] = '</div>';
368 return implode(LF, $content);
369 }
370
371 /**
372 * Wrap content in a field set
373 *
374 * @param string $content Incoming content
375 * @param bool $paletteHidden TRUE if the palette is hidden
376 * @param string $label Given label
377 * @return string Wrapped content
378 */
379 protected function fieldSetWrap($content, $paletteHidden = FALSE, $label = '') {
380 $fieldSetClass = 'form-section';
381 if ($paletteHidden) {
382 $fieldSetClass = 'hide';
383 }
384
385 $result = array();
386 $result[] = '<fieldset class="' . $fieldSetClass . '">';
387
388 if (!empty($label)) {
389 $result[] = '<h4 class="form-section-headline">' . htmlspecialchars($label) . '</h4>';
390 }
391
392 $result[] = $content;
393 $result[] = '</fieldset>';
394 return implode(LF, $result);
395 }
396
397 /**
398 * Wrap a single element
399 *
400 * @param array $element Given element as documented above
401 * @param array $additionalPaletteClasses Additional classes to be added to HTML
402 * @return string Wrapped element
403 */
404 protected function wrapSingleFieldContentWithLabelAndOuterDiv(array $element, array $additionalPaletteClasses = array()) {
405 $fieldName = $element['fieldName'];
406
407 $paletteFieldClasses = array(
408 'form-group',
409 't3js-formengine-palette-field',
410 );
411 foreach ($additionalPaletteClasses as $class) {
412 $paletteFieldClasses[] = $class;
413 }
414
415 $label = BackendUtility::wrapInHelp($this->globalOptions['table'], $fieldName, htmlspecialchars($element['fieldLabel']));
416
417 $content = array();
418 $content[] = '<div class="' . implode(' ', $paletteFieldClasses) . '">';
419 $content[] = '<label class="t3js-formengine-label">';
420 $content[] = $label;
421 $content[] = '<img name="req_' . $this->globalOptions['table'] . '_' . $this->globalOptions['databaseRow']['uid'] . '_' . $fieldName . '" src="clear.gif" class="t3js-formengine-field-required" alt="" />';
422 $content[] = '</label>';
423 $content[] = $element['fieldHtml'];
424 $content[] = '</div>';
425
426 return implode(LF, $content);
427 }
428
429 /**
430 * Determine label of a single field (not a palette label)
431 *
432 * @param string $fieldName The field name to calculate the label for
433 * @param string $labelFromShowItem Given label, typically from show item configuration
434 * @return string Field label
435 */
436 protected function getSingleFieldLabel($fieldName, $labelFromShowItem) {
437 $languageService = $this->getLanguageService();
438 $table = $this->globalOptions['table'];
439 $label = $labelFromShowItem;
440 if (!empty($GLOBALS['TCA'][$table]['columns'][$fieldName]['label'])) {
441 $label = $GLOBALS['TCA'][$table]['columns'][$fieldName]['label'];
442 }
443 if (!empty($labelFromShowItem)) {
444 $label = $labelFromShowItem;
445 }
446 $fieldTSConfig = FormEngineUtility::getTSconfigForTableRow($table, $this->globalOptions['databaseRow'], $fieldName);
447 if (!empty($fieldTSConfig['label'])) {
448 $label = $fieldTSConfig['label'];
449 }
450 if (!empty($fieldTSConfig['label.'][$languageService->lang])) {
451 $label = $fieldTSConfig['label.'][$languageService->lang];
452 }
453 return $languageService->sL($label);
454 }
455
456 /**
457 * TRUE if field is of type user and to wrapping is requested
458 *
459 * @param array $element Current element from "target structure" array
460 * @return boolean TRUE if user and noTableWrapping is set
461 */
462 protected function isUserNoTableWrappingField($element) {
463 $table = $this->globalOptions['table'];
464 $fieldName = $element['fieldName'];
465 if (
466 $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['type'] === 'user'
467 && !empty($GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['noTableWrapping'])
468 ) {
469 return TRUE;
470 }
471 return FALSE;
472 }
473
474 /**
475 * @return LanguageService
476 */
477 protected function getLanguageService() {
478 return $GLOBALS['LANG'];
479 }
480
481 }