[BUGFIX] Fix override field handling in form engine
[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
23 /**
24 * Handle palettes and single fields.
25 *
26 * This container is called by TabsContainer, NoTabsContainer and ListOfFieldsContainer.
27 *
28 * This container mostly operates on TCA showItem of a specific type - the value is
29 * coming in from upper containers as "fieldArray". It handles palettes with all its
30 * different options and prepares rendering of single fields for the SingleFieldContainer.
31 */
32 class PaletteAndSingleContainer extends AbstractContainer {
33
34 /**
35 * Final result array accumulating results from children and final HTML
36 *
37 * @var array
38 */
39 protected $resultArray = array();
40
41 /**
42 * Entry method
43 *
44 * @return array As defined in initializeResultArray() of AbstractNode
45 */
46 public function render() {
47 $languageService = $this->getLanguageService();
48 $table = $this->globalOptions['table'];
49
50 /**
51 * The first code block creates a target structure array to later create the final
52 * HTML string. The single fields and sub containers are rendered here already and
53 * other parts of the return array from children except html are accumulated in
54 * $this->resultArray
55 *
56 $targetStructure = array(
57 0 => array(
58 'type' => 'palette',
59 'fieldName' => 'palette1',
60 'fieldLabel' => 'palette1',
61 'elements' => array(
62 0 => array(
63 'type' => 'single',
64 'fieldName' => 'palettenName',
65 'fieldLabel' => 'element1',
66 'fieldHtml' => 'element1',
67 ),
68 1 => array(
69 'type' => 'linebreak',
70 ),
71 2 => array(
72 'type' => 'single',
73 'fieldName' => 'palettenName',
74 'fieldLabel' => 'element2',
75 'fieldHtml' => 'element2',
76 ),
77 ),
78 ),
79 1 => array( // has 2 as "additional palette"
80 'type' => 'single',
81 'fieldName' => 'element3',
82 'fieldLabel' => 'element3',
83 'fieldHtml' => 'element3',
84 ),
85 2 => array( // do only if 1 had result
86 'type' => 'palette2',
87 'fieldName' => 'palette2',
88 'fieldLabel' => '', // label missing because label of 1 is displayed only
89 'canNotCollapse' => TRUE, // An "additional palette" can not be collapsed
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 $options['fieldExtra'] = $fieldConfiguration['fieldExtra'];
138
139 /** @var SingleFieldContainer $singleFieldContainer */
140 $singleFieldContainer = GeneralUtility::makeInstance(SingleFieldContainer::class);
141 $singleFieldContainer->setGlobalOptions($options);
142 $childResultArray = $singleFieldContainer->render();
143
144 if (!empty($childResultArray['html'])) {
145 $mainStructureCounter ++;
146
147 $targetStructure[$mainStructureCounter] = array(
148 'type' => 'single',
149 'fieldName' => $fieldConfiguration['fieldName'],
150 'fieldLabel' => $this->getSingleFieldLabel($fieldName, $fieldConfiguration['fieldLabel']),
151 'fieldHtml' => $childResultArray['html'],
152 );
153
154 // If the third part of a show item field is given, this is a name of a palette that should be rendered
155 // below the single field - without palette header and only if single field produced content
156 if (!empty($childResultArray['html']) && !empty($fieldConfiguration['paletteName'])) {
157 $paletteElementArray = $this->createPaletteContentArray($fieldConfiguration['paletteName']);
158 if (!empty($paletteElementArray)) {
159 $mainStructureCounter ++;
160 $targetStructure[$mainStructureCounter] = array(
161 'type' => 'palette',
162 'fieldName' => $fieldConfiguration['paletteName'],
163 'fieldLabel' => '', // An "additional palette" has no show label
164 'canNotCollapse' => TRUE,
165 'elements' => $paletteElementArray,
166 );
167 }
168 }
169 }
170
171 $childResultArray['html'] = '';
172 $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $childResultArray);
173 }
174 }
175
176 // Compile final content
177 $content = array();
178 foreach ($targetStructure as $element) {
179 if ($element['type'] === 'palette') {
180 $paletteName = $element['fieldName'];
181 $paletteElementsHtml = $this->renderInnerPaletteContent($element);
182
183 $isHiddenPalette = !empty($GLOBALS['TCA'][$table]['palettes'][$paletteName]['isHiddenPalette']);
184
185 $renderUnCollapseButtonWrapper = TRUE;
186 // No button if the palette is hidden
187 if ($isHiddenPalette) {
188 $renderUnCollapseButtonWrapper = FALSE;
189 }
190 // No button if palette can not collapse on ctrl level
191 if (!empty($GLOBALS['TCA'][$table]['ctrl']['canNotCollapse'])) {
192 $renderUnCollapseButtonWrapper = FALSE;
193 }
194 // No button if palette can not collapse on palette definition level
195 if (!empty($GLOBALS['TCA'][$table]['palettes'][$paletteName]['canNotCollapse'])) {
196 $renderUnCollapseButtonWrapper = FALSE;
197 }
198 // No button if palettes are not collapsed - this is the checkbox at the end of the form
199 if (!$this->globalOptions['palettesCollapsed']) {
200 $renderUnCollapseButtonWrapper = FALSE;
201 }
202 // No button if palette is set to no collapse on element level - this is the case if palette is an "additional palette" after a casual field
203 if (!empty($element['canNotCollapse'])) {
204 $renderUnCollapseButtonWrapper = FALSE;
205 }
206
207 if ($renderUnCollapseButtonWrapper) {
208 $cssId = 'FORMENGINE_' . $this->globalOptions['table'] . '_' . $paletteName . '_' . $this->globalOptions['databaseRow']['uid'];
209 $paletteElementsHtml = $this->wrapPaletteWithCollapseButton($paletteElementsHtml, $cssId);
210 } else {
211 $paletteElementsHtml = '<div class="row">' . $paletteElementsHtml . '</div>';
212 }
213
214 $content[] = $this->fieldSetWrap($paletteElementsHtml, $isHiddenPalette, $element['fieldLabel']);
215 } else {
216 // Return raw HTML only in case of user element with no wrapping requested
217 if ($this->isUserNoTableWrappingField($element)) {
218 $content[] = $element['fieldHtml'];
219 } else {
220 $content[] = $this->fieldSetWrap($this->wrapSingleFieldContent($element));
221 }
222 }
223 }
224
225 $finalResultArray = $this->resultArray;
226 $finalResultArray['html'] = implode(LF, $content);
227 return $finalResultArray;
228 }
229
230 /**
231 * Render single fields of a given palette
232 *
233 * @param string $paletteName The palette to render
234 * @return array
235 */
236 protected function createPaletteContentArray($paletteName) {
237 $table = $this->globalOptions['table'];
238 $excludeElements = $this->globalOptions['excludeElements'];
239
240 // palette needs a palette name reference, otherwise it does not make sense to try rendering of it
241 if (empty($paletteName) || empty($GLOBALS['TCA'][$table]['palettes'][$paletteName]['showitem'])) {
242 return array();
243 }
244
245 $resultStructure = array();
246 $foundRealElement = FALSE; // Set to true if not only line breaks were rendered
247 $fieldsArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['palettes'][$paletteName]['showitem'], TRUE);
248 foreach ($fieldsArray as $fieldString) {
249 $fieldArray = $this->explodeSingleFieldShowItemConfiguration($fieldString);
250 $fieldName = $fieldArray['fieldName'];
251 if ($fieldName === '--linebreak--') {
252 $resultStructure[] = array(
253 'type' => 'linebreak',
254 );
255 } else {
256 if (in_array($fieldName, $excludeElements, TRUE) || !is_array($GLOBALS['TCA'][$table]['columns'][$fieldName])) {
257 continue;
258 }
259 $options = $this->globalOptions;
260 $options['fieldName'] = $fieldName;
261 $options['fieldExtra'] = $fieldArray['fieldExtra'];
262
263 /** @var SingleFieldContainer $singleFieldContainer */
264 $singleFieldContainer = GeneralUtility::makeInstance(SingleFieldContainer::class);
265 $singleFieldContainer->setGlobalOptions($options);
266 $singleFieldContentArray = $singleFieldContainer->render();
267
268 if (!empty($singleFieldContentArray['html'])) {
269 $foundRealElement = TRUE;
270 $resultStructure[] = array(
271 'type' => 'single',
272 'fieldName' => $fieldName,
273 'fieldLabel' => $this->getSingleFieldLabel($fieldName, $fieldArray['fieldLabel']),
274 'fieldHtml' => $singleFieldContentArray['html'],
275 );
276 $singleFieldContentArray['html'] = '';
277 $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $singleFieldContentArray);
278 } else {
279 $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $singleFieldContentArray);
280 }
281 }
282 }
283
284 if ($foundRealElement) {
285 return $resultStructure;
286 } else {
287 return array();
288 }
289 }
290
291 /**
292 * Renders inner content of single elements of a palette and wrap it as needed
293 *
294 * @param array $elementArray Array of elements
295 * @return string Wrapped content
296 */
297 protected function renderInnerPaletteContent(array $elementArray) {
298 // Group fields
299 $groupedFields = array();
300 $row = 0;
301 $lastLineWasLinebreak = TRUE;
302 foreach ($elementArray['elements'] as $element) {
303 if ($element['type'] === 'linebreak') {
304 if (!$lastLineWasLinebreak) {
305 $row++;
306 $groupedFields[$row][] = $element;
307 $row++;
308 $lastLineWasLinebreak = TRUE;
309 }
310 } else {
311 $lastLineWasLinebreak = FALSE;
312 $groupedFields[$row][] = $element;
313 }
314 }
315
316 $result = array();
317 // Process fields
318 foreach ($groupedFields as $fields) {
319 $numberOfItems = count($fields);
320 $colWidth = (int)floor(12 / $numberOfItems);
321 // Column class calculation
322 $colClass = "col-md-12";
323 $colClear = array();
324 if ($colWidth == 6) {
325 $colClass = "col-sm-6";
326 $colClear = array(
327 2 => 'visible-sm-block visible-md-block visible-lg-block',
328 );
329 } elseif ($colWidth === 4) {
330 $colClass = "col-sm-4";
331 $colClear = array(
332 3 => 'visible-sm-block visible-md-block visible-lg-block',
333 );
334 } elseif ($colWidth === 3) {
335 $colClass = "col-sm-6 col-md-3";
336 $colClear = array(
337 2 => 'visible-sm-block',
338 4 => 'visible-md-block visible-lg-block',
339 );
340 } elseif ($colWidth <= 2) {
341 $colClass = "checkbox-column col-sm-6 col-md-3 col-lg-2";
342 $colClear = array(
343 2 => 'visible-sm-block',
344 4 => 'visible-md-block',
345 6 => 'visible-lg-block'
346 );
347 }
348
349 // Render fields
350 for ($counter = 0; $counter < $numberOfItems; $counter++) {
351 $element = $fields[$counter];
352 if ($element['type'] === 'linebreak') {
353 if ($counter !== $numberOfItems) {
354 $result[] = '<div class="clearfix"></div>';
355 }
356 } else {
357 $result[] = $this->wrapSingleFieldContent($element, array($colClass));
358
359 // Breakpoints
360 if ($counter + 1 < $numberOfItems && !empty($colClear)) {
361 foreach ($colClear as $rowBreakAfter => $clearClass) {
362 if (($counter + 1) % $rowBreakAfter === 0) {
363 $result[] = '<div class="clearfix '. $clearClass . '"></div>';
364 }
365 }
366 }
367 }
368 }
369 }
370
371 return implode(LF, $result);
372 }
373
374 /**
375 * Add a "collapsible" button around given content
376 *
377 * @param string $elementHtml HTML of handled palette content
378 * @param string $cssId A css id to be added
379 * @return string Wrapped content
380 */
381 protected function wrapPaletteWithCollapseButton($elementHtml, $cssId) {
382 $content = array();
383 $content[] = '<p>';
384 $content[] = '<button class="btn btn-default" type="button" data-toggle="collapse" data-target="#' . $cssId . '" aria-expanded="false" aria-controls="' . $cssId . '">';
385 $content[] = IconUtility::getSpriteIcon('actions-system-options-view');
386 $content[] = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.moreOptions'));
387 $content[] = '</button>';
388 $content[] = '</p>';
389 $content[] = '<div id="' . $cssId . '" class="form-section-collapse collapse">';
390 $content[] = '<div class="row">' . $elementHtml . '</div>';
391 $content[] = '</div>';
392 return implode(LF, $content);
393 }
394
395 /**
396 * Wrap content in a field set
397 *
398 * @param string $content Incoming content
399 * @param bool $paletteHidden TRUE if the palette is hidden
400 * @param string $label Given label
401 * @return string Wrapped content
402 */
403 protected function fieldSetWrap($content, $paletteHidden = FALSE, $label = '') {
404 $fieldSetClass = 'form-section';
405 if ($paletteHidden) {
406 $fieldSetClass = 'hide';
407 }
408
409 $result = array();
410 $result[] = '<fieldset class="' . $fieldSetClass . '">';
411
412 if (!empty($label)) {
413 $result[] = '<h4 class="form-section-headline">' . htmlspecialchars($label) . '</h4>';
414 }
415
416 $result[] = $content;
417 $result[] = '</fieldset>';
418 return implode(LF, $result);
419 }
420
421 /**
422 * Wrap a single element
423 *
424 * @param array $element Given element as documented above
425 * @param array $additionalPaletteClasses Additional classes to be added to HTML
426 * @return string Wrapped element
427 */
428 protected function wrapSingleFieldContent(array $element, array $additionalPaletteClasses = array()) {
429 $fieldName = $element['fieldName'];
430
431 $paletteFieldClasses = array(
432 'form-group',
433 't3js-formengine-palette-field',
434 );
435 foreach ($additionalPaletteClasses as $class) {
436 $paletteFieldClasses[] = $class;
437 }
438
439 $fieldItemClasses = array(
440 't3js-formengine-field-item'
441 );
442 $isNullValueField = $this->isDisabledNullValueField($fieldName);
443 if ($isNullValueField) {
444 $fieldItemClasses[] = 'disabled';
445 }
446
447 $label = BackendUtility::wrapInHelp($this->globalOptions['table'], $fieldName, htmlspecialchars($element['fieldLabel']));
448
449 $content = array();
450 $content[] = '<div class="' . implode(' ', $paletteFieldClasses) . '">';
451 $content[] = '<label class="t3js-formengine-label">';
452 $content[] = $label;
453 $content[] = '<img name="req_' . $this->globalOptions['table'] . '_' . $this->globalOptions['databaseRow']['uid'] . '_' . $fieldName . '" src="clear.gif" class="t3js-formengine-field-required" alt="" />';
454 $content[] = '</label>';
455 $content[] = '<div class="' . implode(' ', $fieldItemClasses) . '">';
456 $content[] = '<div class="t3-form-field-disable"></div>';
457 $content[] = $this->renderNullValueWidget($fieldName);
458 $content[] = $element['fieldHtml'];
459 $content[] = '</div>';
460 $content[] = '</div>';
461
462 return implode(LF, $content);
463 }
464
465 /**
466 * Determine label of a single field (not a palette label)
467 *
468 * @param string $fieldName The field name to calculate the label for
469 * @param string $labelFromShowItem Given label, typically from show item configuration
470 * @return string Field label
471 */
472 protected function getSingleFieldLabel($fieldName, $labelFromShowItem) {
473 $languageService = $this->getLanguageService();
474 $table = $this->globalOptions['table'];
475 $label = $labelFromShowItem;
476 if (!empty($GLOBALS['TCA'][$table]['columns'][$fieldName]['label'])) {
477 $label = $GLOBALS['TCA'][$table]['columns'][$fieldName]['label'];
478 }
479 if (!empty($labelFromShowItem)) {
480 $label = $labelFromShowItem;
481 }
482 $fieldTSConfig = FormEngineUtility::getTSconfigForTableRow($table, $this->globalOptions['databaseRow'], $fieldName);
483 if (!empty($fieldTSConfig['label'])) {
484 $label = $fieldTSConfig['label'];
485 }
486 if (!empty($fieldTSConfig['label.'][$languageService->lang])) {
487 $label = $fieldTSConfig['label.'][$languageService->lang];
488 }
489 return $languageService->sL($label);
490 }
491
492 /**
493 * TRUE if field is of type user and to wrapping is requested
494 *
495 * @param array $element Current element from "target structure" array
496 * @return boolean TRUE if user and noTableWrapping is set
497 */
498 protected function isUserNoTableWrappingField($element) {
499 $table = $this->globalOptions['table'];
500 $fieldName = $element['fieldName'];
501 if (
502 $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['type'] === 'user'
503 && !empty($GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['noTableWrapping'])
504 ) {
505 return TRUE;
506 }
507 return FALSE;
508 }
509
510 /**
511 * Determines whether the current field value is considered as NULL value.
512 * Using NULL values is enabled by using 'null' in the 'eval' TCA definition.
513 * If NULL value is possible for a field and additional checkbox next to the element will be rendered.
514 *
515 * @param string $fieldName The field to handle
516 * @return bool
517 */
518 protected function isDisabledNullValueField($fieldName) {
519 $table = $this->globalOptions['table'];
520 $row = $this->globalOptions['databaseRow'];
521 $config = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
522 $result = FALSE;
523 $value = $row[$fieldName];
524 if (
525 $value === NULL
526 && !empty($config['eval']) && GeneralUtility::inList($config['eval'], 'null')
527 && (empty($config['mode']) || $config['mode'] !== 'useOrOverridePlaceholder')
528 ) {
529 $result = TRUE;
530 }
531 return $result;
532 }
533
534 /**
535 * Renders a view widget to handle and activate NULL values.
536 * The widget is enabled by using 'null' in the 'eval' TCA definition.
537 *
538 * @param string $fieldName The field to handle
539 * @return string
540 */
541 protected function renderNullValueWidget($fieldName) {
542 $table = $this->globalOptions['table'];
543 $row = $this->globalOptions['databaseRow'];
544 $value = $row[$fieldName];
545 $config = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
546
547 $widget = array();
548 // Checkbox should be rendered if eval null set and no override stuff is done
549 if (
550 !empty($config['eval']) && GeneralUtility::inList($config['eval'], 'null')
551 && (empty($config['mode']) || $config['mode'] !== 'useOrOverridePlaceholder')
552 ) {
553 $checked = $value === NULL ? '' : ' checked="checked"';
554 $formElementName = $this->globalOptions['prependFormFieldNames'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
555 $formElementNameActive = $this->globalOptions['prependFormFieldNamesActive'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
556 $onChange = htmlspecialchars(
557 'typo3form.fieldSetNull(\'' . $formElementName . '\', !this.checked)'
558 );
559
560 $widget = array();
561 $widget[] = '<div class="checkbox">';
562 $widget[] = '<label>';
563 $widget[] = '<input type="hidden" name="' . $formElementNameActive . '" value="0" />';
564 $widget[] = '<input type="checkbox" name="' . $formElementNameActive . '" value="1" onchange="' . $onChange . '"' . $checked . ' /> &nbsp;';
565 $widget[] = '</label>';
566 $widget[] = '</div>';
567 }
568
569 return implode(LF, $widget);
570 }
571
572 /**
573 * @return LanguageService
574 */
575 protected function getLanguageService() {
576 return $GLOBALS['LANG'];
577 }
578
579 }