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