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