[BUGFIX] Override flex form field label with page TSConfig
[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 $fieldLabel = '';
141 if (!empty($this->data['processedTca']['columns'][$fieldName]['label'])) {
142 $fieldLabel = $this->data['processedTca']['columns'][$fieldName]['label'];
143 }
144 $targetStructure[$mainStructureCounter] = array(
145 'type' => 'single',
146 'fieldName' => $fieldConfiguration['fieldName'],
147 'fieldLabel' => $fieldLabel,
148 'fieldHtml' => $childResultArray['html'],
149 );
150 }
151
152 $childResultArray['html'] = '';
153 $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $childResultArray);
154 }
155 }
156
157 // Compile final content
158 $content = array();
159 foreach ($targetStructure as $element) {
160 if ($element['type'] === 'palette') {
161 $paletteName = $element['fieldName'];
162 $paletteElementsHtml = $this->renderInnerPaletteContent($element);
163
164 $isHiddenPalette = !empty($this->data['processedTca']['palettes'][$paletteName]['isHiddenPalette']);
165
166 $paletteElementsHtml = '<div class="row">' . $paletteElementsHtml . '</div>';
167
168 $content[] = $this->fieldSetWrap($paletteElementsHtml, $isHiddenPalette, $element['fieldLabel']);
169 } else {
170 // Return raw HTML only in case of user element with no wrapping requested
171 if ($this->isUserNoTableWrappingField($element)) {
172 $content[] = $element['fieldHtml'];
173 } else {
174 $content[] = $this->fieldSetWrap($this->wrapSingleFieldContentWithLabelAndOuterDiv($element));
175 }
176 }
177 }
178
179 $finalResultArray = $this->resultArray;
180 $finalResultArray['html'] = implode(LF, $content);
181 return $finalResultArray;
182 }
183
184 /**
185 * Render single fields of a given palette
186 *
187 * @param string $paletteName The palette to render
188 * @return array
189 */
190 protected function createPaletteContentArray($paletteName)
191 {
192 // palette needs a palette name reference, otherwise it does not make sense to try rendering of it
193 if (empty($paletteName) || empty($this->data['processedTca']['palettes'][$paletteName]['showitem'])) {
194 return array();
195 }
196
197 $resultStructure = array();
198 $foundRealElement = false; // Set to true if not only line breaks were rendered
199 $fieldsArray = GeneralUtility::trimExplode(',', $this->data['processedTca']['palettes'][$paletteName]['showitem'], true);
200 foreach ($fieldsArray as $fieldString) {
201 $fieldArray = $this->explodeSingleFieldShowItemConfiguration($fieldString);
202 $fieldName = $fieldArray['fieldName'];
203 if ($fieldName === '--linebreak--') {
204 $resultStructure[] = array(
205 'type' => 'linebreak',
206 );
207 } else {
208 if (!is_array($this->data['processedTca']['columns'][$fieldName])) {
209 continue;
210 }
211 $options = $this->data;
212 $options['fieldName'] = $fieldName;
213
214 $options['renderType'] = 'singleFieldContainer';
215 $singleFieldContentArray = $this->nodeFactory->create($options)->render();
216
217 if (!empty($singleFieldContentArray['html'])) {
218 $foundRealElement = true;
219 $fieldLabel = '';
220 if (!empty($this->data['processedTca']['columns'][$fieldName]['label'])) {
221 $fieldLabel = $this->data['processedTca']['columns'][$fieldName]['label'];
222 }
223 $resultStructure[] = array(
224 'type' => 'single',
225 'fieldName' => $fieldName,
226 'fieldLabel' => $fieldLabel,
227 'fieldHtml' => $singleFieldContentArray['html'],
228 );
229 $singleFieldContentArray['html'] = '';
230 }
231 $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $singleFieldContentArray);
232 }
233 }
234
235 if ($foundRealElement) {
236 return $resultStructure;
237 } else {
238 return array();
239 }
240 }
241
242 /**
243 * Renders inner content of single elements of a palette and wrap it as needed
244 *
245 * @param array $elementArray Array of elements
246 * @return string Wrapped content
247 */
248 protected function renderInnerPaletteContent(array $elementArray)
249 {
250 // Group fields
251 $groupedFields = array();
252 $row = 0;
253 $lastLineWasLinebreak = true;
254 foreach ($elementArray['elements'] as $element) {
255 if ($element['type'] === 'linebreak') {
256 if (!$lastLineWasLinebreak) {
257 $row++;
258 $groupedFields[$row][] = $element;
259 $row++;
260 $lastLineWasLinebreak = true;
261 }
262 } else {
263 $lastLineWasLinebreak = false;
264 $groupedFields[$row][] = $element;
265 }
266 }
267
268 $result = array();
269 // Process fields
270 foreach ($groupedFields as $fields) {
271 $numberOfItems = count($fields);
272 $colWidth = (int)floor(12 / $numberOfItems);
273 // Column class calculation
274 $colClass = 'col-md-12';
275 $colClear = array();
276 if ($colWidth == 6) {
277 $colClass = 'col-sm-6';
278 $colClear = array(
279 2 => 'visible-sm-block visible-md-block visible-lg-block',
280 );
281 } elseif ($colWidth === 4) {
282 $colClass = 'col-sm-4';
283 $colClear = array(
284 3 => 'visible-sm-block visible-md-block visible-lg-block',
285 );
286 } elseif ($colWidth === 3) {
287 $colClass = 'col-sm-6 col-md-3';
288 $colClear = array(
289 2 => 'visible-sm-block',
290 4 => 'visible-md-block visible-lg-block',
291 );
292 } elseif ($colWidth <= 2) {
293 $colClass = 'checkbox-column col-sm-6 col-md-3 col-lg-2';
294 $colClear = array(
295 2 => 'visible-sm-block',
296 4 => 'visible-md-block',
297 6 => 'visible-lg-block'
298 );
299 }
300
301 // Render fields
302 for ($counter = 0; $counter < $numberOfItems; $counter++) {
303 $element = $fields[$counter];
304 if ($element['type'] === 'linebreak') {
305 if ($counter !== $numberOfItems) {
306 $result[] = '<div class="clearfix"></div>';
307 }
308 } else {
309 $result[] = $this->wrapSingleFieldContentWithLabelAndOuterDiv($element, array($colClass));
310
311 // Breakpoints
312 if ($counter + 1 < $numberOfItems && !empty($colClear)) {
313 foreach ($colClear as $rowBreakAfter => $clearClass) {
314 if (($counter + 1) % $rowBreakAfter === 0) {
315 $result[] = '<div class="clearfix ' . $clearClass . '"></div>';
316 }
317 }
318 }
319 }
320 }
321 }
322
323 return implode(LF, $result);
324 }
325
326 /**
327 * Wrap content in a field set
328 *
329 * @param string $content Incoming content
330 * @param bool $paletteHidden TRUE if the palette is hidden
331 * @param string $label Given label
332 * @return string Wrapped content
333 */
334 protected function fieldSetWrap($content, $paletteHidden = false, $label = '')
335 {
336 $fieldSetClass = 'form-section';
337 if ($paletteHidden) {
338 $fieldSetClass = 'hide';
339 }
340
341 $result = array();
342 $result[] = '<fieldset class="' . $fieldSetClass . '">';
343
344 if (!empty($label)) {
345 $result[] = '<h4 class="form-section-headline">' . htmlspecialchars($label) . '</h4>';
346 }
347
348 $result[] = $content;
349 $result[] = '</fieldset>';
350 return implode(LF, $result);
351 }
352
353 /**
354 * Wrap a single element
355 *
356 * @param array $element Given element as documented above
357 * @param array $additionalPaletteClasses Additional classes to be added to HTML
358 * @return string Wrapped element
359 */
360 protected function wrapSingleFieldContentWithLabelAndOuterDiv(array $element, array $additionalPaletteClasses = array())
361 {
362 $fieldName = $element['fieldName'];
363
364 $paletteFieldClasses = array(
365 'form-group',
366 't3js-formengine-validation-marker',
367 't3js-formengine-palette-field',
368 );
369 foreach ($additionalPaletteClasses as $class) {
370 $paletteFieldClasses[] = $class;
371 }
372
373 $label = BackendUtility::wrapInHelp($this->data['tableName'], $fieldName, htmlspecialchars($element['fieldLabel']));
374
375 $content = array();
376 $content[] = '<div class="' . implode(' ', $paletteFieldClasses) . '">';
377 $content[] = '<label class="t3js-formengine-label">';
378 $content[] = $label;
379 $content[] = '</label>';
380 $content[] = $element['fieldHtml'];
381 $content[] = '</div>';
382
383 return implode(LF, $content);
384 }
385
386 /**
387 * TRUE if field is of type user and to wrapping is requested
388 *
389 * @param array $element Current element from "target structure" array
390 * @return bool TRUE if user and noTableWrapping is set
391 */
392 protected function isUserNoTableWrappingField($element)
393 {
394 $fieldName = $element['fieldName'];
395 if (
396 $this->data['processedTca']['columns'][$fieldName]['config']['type'] === 'user'
397 && !empty($this->data['processedTca']['columns'][$fieldName]['config']['noTableWrapping'])
398 ) {
399 return true;
400 }
401 return false;
402 }
403
404 /**
405 * @return LanguageService
406 */
407 protected function getLanguageService()
408 {
409 return $GLOBALS['LANG'];
410 }
411 }