[FOLLOWUP] 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 }
278 $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $singleFieldContentArray);
279 }
280 }
281
282 if ($foundRealElement) {
283 return $resultStructure;
284 } else {
285 return array();
286 }
287 }
288
289 /**
290 * Renders inner content of single elements of a palette and wrap it as needed
291 *
292 * @param array $elementArray Array of elements
293 * @return string Wrapped content
294 */
295 protected function renderInnerPaletteContent(array $elementArray) {
296 // Group fields
297 $groupedFields = array();
298 $row = 0;
299 $lastLineWasLinebreak = TRUE;
300 foreach ($elementArray['elements'] as $element) {
301 if ($element['type'] === 'linebreak') {
302 if (!$lastLineWasLinebreak) {
303 $row++;
304 $groupedFields[$row][] = $element;
305 $row++;
306 $lastLineWasLinebreak = TRUE;
307 }
308 } else {
309 $lastLineWasLinebreak = FALSE;
310 $groupedFields[$row][] = $element;
311 }
312 }
313
314 $result = array();
315 // Process fields
316 foreach ($groupedFields as $fields) {
317 $numberOfItems = count($fields);
318 $colWidth = (int)floor(12 / $numberOfItems);
319 // Column class calculation
320 $colClass = "col-md-12";
321 $colClear = array();
322 if ($colWidth == 6) {
323 $colClass = "col-sm-6";
324 $colClear = array(
325 2 => 'visible-sm-block visible-md-block visible-lg-block',
326 );
327 } elseif ($colWidth === 4) {
328 $colClass = "col-sm-4";
329 $colClear = array(
330 3 => 'visible-sm-block visible-md-block visible-lg-block',
331 );
332 } elseif ($colWidth === 3) {
333 $colClass = "col-sm-6 col-md-3";
334 $colClear = array(
335 2 => 'visible-sm-block',
336 4 => 'visible-md-block visible-lg-block',
337 );
338 } elseif ($colWidth <= 2) {
339 $colClass = "checkbox-column col-sm-6 col-md-3 col-lg-2";
340 $colClear = array(
341 2 => 'visible-sm-block',
342 4 => 'visible-md-block',
343 6 => 'visible-lg-block'
344 );
345 }
346
347 // Render fields
348 for ($counter = 0; $counter < $numberOfItems; $counter++) {
349 $element = $fields[$counter];
350 if ($element['type'] === 'linebreak') {
351 if ($counter !== $numberOfItems) {
352 $result[] = '<div class="clearfix"></div>';
353 }
354 } else {
355 $result[] = $this->wrapSingleFieldContent($element, array($colClass));
356
357 // Breakpoints
358 if ($counter + 1 < $numberOfItems && !empty($colClear)) {
359 foreach ($colClear as $rowBreakAfter => $clearClass) {
360 if (($counter + 1) % $rowBreakAfter === 0) {
361 $result[] = '<div class="clearfix '. $clearClass . '"></div>';
362 }
363 }
364 }
365 }
366 }
367 }
368
369 return implode(LF, $result);
370 }
371
372 /**
373 * Add a "collapsible" button around given content
374 *
375 * @param string $elementHtml HTML of handled palette content
376 * @param string $cssId A css id to be added
377 * @return string Wrapped content
378 */
379 protected function wrapPaletteWithCollapseButton($elementHtml, $cssId) {
380 $content = array();
381 $content[] = '<p>';
382 $content[] = '<button class="btn btn-default" type="button" data-toggle="collapse" data-target="#' . $cssId . '" aria-expanded="false" aria-controls="' . $cssId . '">';
383 $content[] = IconUtility::getSpriteIcon('actions-system-options-view');
384 $content[] = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.moreOptions'));
385 $content[] = '</button>';
386 $content[] = '</p>';
387 $content[] = '<div id="' . $cssId . '" class="form-section-collapse collapse">';
388 $content[] = '<div class="row">' . $elementHtml . '</div>';
389 $content[] = '</div>';
390 return implode(LF, $content);
391 }
392
393 /**
394 * Wrap content in a field set
395 *
396 * @param string $content Incoming content
397 * @param bool $paletteHidden TRUE if the palette is hidden
398 * @param string $label Given label
399 * @return string Wrapped content
400 */
401 protected function fieldSetWrap($content, $paletteHidden = FALSE, $label = '') {
402 $fieldSetClass = 'form-section';
403 if ($paletteHidden) {
404 $fieldSetClass = 'hide';
405 }
406
407 $result = array();
408 $result[] = '<fieldset class="' . $fieldSetClass . '">';
409
410 if (!empty($label)) {
411 $result[] = '<h4 class="form-section-headline">' . htmlspecialchars($label) . '</h4>';
412 }
413
414 $result[] = $content;
415 $result[] = '</fieldset>';
416 return implode(LF, $result);
417 }
418
419 /**
420 * Wrap a single element
421 *
422 * @param array $element Given element as documented above
423 * @param array $additionalPaletteClasses Additional classes to be added to HTML
424 * @return string Wrapped element
425 */
426 protected function wrapSingleFieldContent(array $element, array $additionalPaletteClasses = array()) {
427 $fieldName = $element['fieldName'];
428
429 $paletteFieldClasses = array(
430 'form-group',
431 't3js-formengine-palette-field',
432 );
433 foreach ($additionalPaletteClasses as $class) {
434 $paletteFieldClasses[] = $class;
435 }
436
437 $fieldItemClasses = array(
438 't3js-formengine-field-item'
439 );
440 $isNullValueField = $this->isDisabledNullValueField($fieldName);
441 if ($isNullValueField) {
442 $fieldItemClasses[] = 'disabled';
443 }
444
445 $label = BackendUtility::wrapInHelp($this->globalOptions['table'], $fieldName, htmlspecialchars($element['fieldLabel']));
446
447 $content = array();
448 $content[] = '<div class="' . implode(' ', $paletteFieldClasses) . '">';
449 $content[] = '<label class="t3js-formengine-label">';
450 $content[] = $label;
451 $content[] = '<img name="req_' . $this->globalOptions['table'] . '_' . $this->globalOptions['databaseRow']['uid'] . '_' . $fieldName . '" src="clear.gif" class="t3js-formengine-field-required" alt="" />';
452 $content[] = '</label>';
453 $content[] = '<div class="' . implode(' ', $fieldItemClasses) . '">';
454 $content[] = '<div class="t3-form-field-disable"></div>';
455 $content[] = $this->renderNullValueWidget($fieldName);
456 $content[] = $element['fieldHtml'];
457 $content[] = '</div>';
458 $content[] = '</div>';
459
460 return implode(LF, $content);
461 }
462
463 /**
464 * Determine label of a single field (not a palette label)
465 *
466 * @param string $fieldName The field name to calculate the label for
467 * @param string $labelFromShowItem Given label, typically from show item configuration
468 * @return string Field label
469 */
470 protected function getSingleFieldLabel($fieldName, $labelFromShowItem) {
471 $languageService = $this->getLanguageService();
472 $table = $this->globalOptions['table'];
473 $label = $labelFromShowItem;
474 if (!empty($GLOBALS['TCA'][$table]['columns'][$fieldName]['label'])) {
475 $label = $GLOBALS['TCA'][$table]['columns'][$fieldName]['label'];
476 }
477 if (!empty($labelFromShowItem)) {
478 $label = $labelFromShowItem;
479 }
480 $fieldTSConfig = FormEngineUtility::getTSconfigForTableRow($table, $this->globalOptions['databaseRow'], $fieldName);
481 if (!empty($fieldTSConfig['label'])) {
482 $label = $fieldTSConfig['label'];
483 }
484 if (!empty($fieldTSConfig['label.'][$languageService->lang])) {
485 $label = $fieldTSConfig['label.'][$languageService->lang];
486 }
487 return $languageService->sL($label);
488 }
489
490 /**
491 * TRUE if field is of type user and to wrapping is requested
492 *
493 * @param array $element Current element from "target structure" array
494 * @return boolean TRUE if user and noTableWrapping is set
495 */
496 protected function isUserNoTableWrappingField($element) {
497 $table = $this->globalOptions['table'];
498 $fieldName = $element['fieldName'];
499 if (
500 $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['type'] === 'user'
501 && !empty($GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['noTableWrapping'])
502 ) {
503 return TRUE;
504 }
505 return FALSE;
506 }
507
508 /**
509 * Determines whether the current field value is considered as NULL value.
510 * Using NULL values is enabled by using 'null' in the 'eval' TCA definition.
511 * If NULL value is possible for a field and additional checkbox next to the element will be rendered.
512 *
513 * @param string $fieldName The field to handle
514 * @return bool
515 */
516 protected function isDisabledNullValueField($fieldName) {
517 $table = $this->globalOptions['table'];
518 $row = $this->globalOptions['databaseRow'];
519 $config = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
520 $result = FALSE;
521 $value = $row[$fieldName];
522 if (
523 $value === NULL
524 && !empty($config['eval']) && GeneralUtility::inList($config['eval'], 'null')
525 && (empty($config['mode']) || $config['mode'] !== 'useOrOverridePlaceholder')
526 ) {
527 $result = TRUE;
528 }
529 return $result;
530 }
531
532 /**
533 * Renders a view widget to handle and activate NULL values.
534 * The widget is enabled by using 'null' in the 'eval' TCA definition.
535 *
536 * @param string $fieldName The field to handle
537 * @return string
538 */
539 protected function renderNullValueWidget($fieldName) {
540 $table = $this->globalOptions['table'];
541 $row = $this->globalOptions['databaseRow'];
542 $value = $row[$fieldName];
543 $config = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
544
545 $widget = array();
546 // Checkbox should be rendered if eval null set and no override stuff is done
547 if (
548 !empty($config['eval']) && GeneralUtility::inList($config['eval'], 'null')
549 && (empty($config['mode']) || $config['mode'] !== 'useOrOverridePlaceholder')
550 ) {
551 $checked = $value === NULL ? '' : ' checked="checked"';
552 $formElementName = $this->globalOptions['prependFormFieldNames'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
553 $formElementNameActive = $this->globalOptions['prependFormFieldNamesActive'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
554 $onChange = htmlspecialchars(
555 'typo3form.fieldSetNull(\'' . $formElementName . '\', !this.checked)'
556 );
557
558 $widget = array();
559 $widget[] = '<div class="checkbox">';
560 $widget[] = '<label>';
561 $widget[] = '<input type="hidden" name="' . $formElementNameActive . '" value="0" />';
562 $widget[] = '<input type="checkbox" name="' . $formElementNameActive . '" value="1" onchange="' . $onChange . '"' . $checked . ' /> &nbsp;';
563 $widget[] = '</label>';
564 $widget[] = '</div>';
565 }
566
567 return implode(LF, $widget);
568 }
569
570 /**
571 * @return LanguageService
572 */
573 protected function getLanguageService() {
574 return $GLOBALS['LANG'];
575 }
576
577 }