[BUGFIX] Palettes "more options" can not be extended if new record is added
[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 use TYPO3\CMS\Backend\Form\NodeFactory;
23
24 /**
25 * Handle palettes and single fields.
26 *
27 * This container is called by TabsContainer, NoTabsContainer and ListOfFieldsContainer.
28 *
29 * This container mostly operates on TCA showItem of a specific type - the value is
30 * coming in from upper containers as "fieldArray". It handles palettes with all its
31 * different options and prepares rendering of single fields for the SingleFieldContainer.
32 */
33 class PaletteAndSingleContainer extends AbstractContainer {
34
35 /**
36 * Final result array accumulating results from children and final HTML
37 *
38 * @var array
39 */
40 protected $resultArray = array();
41
42 /**
43 * Entry method
44 *
45 * @return array As defined in initializeResultArray() of AbstractNode
46 */
47 public function render() {
48 $languageService = $this->getLanguageService();
49 $table = $this->globalOptions['table'];
50
51 /**
52 * The first code block creates a target structure array to later create the final
53 * HTML string. The single fields and sub containers are rendered here already and
54 * other parts of the return array from children except html are accumulated in
55 * $this->resultArray
56 *
57 $targetStructure = array(
58 0 => array(
59 'type' => 'palette',
60 'fieldName' => 'palette1',
61 'fieldLabel' => 'palette1',
62 'elements' => array(
63 0 => array(
64 'type' => 'single',
65 'fieldName' => 'palettenName',
66 'fieldLabel' => 'element1',
67 'fieldHtml' => 'element1',
68 ),
69 1 => array(
70 'type' => 'linebreak',
71 ),
72 2 => array(
73 'type' => 'single',
74 'fieldName' => 'palettenName',
75 'fieldLabel' => 'element2',
76 'fieldHtml' => 'element2',
77 ),
78 ),
79 ),
80 1 => array( // has 2 as "additional palette"
81 'type' => 'single',
82 'fieldName' => 'element3',
83 'fieldLabel' => 'element3',
84 'fieldHtml' => 'element3',
85 ),
86 2 => array( // do only if 1 had result
87 'type' => 'palette2',
88 'fieldName' => 'palette2',
89 'fieldLabel' => '', // label missing because label of 1 is displayed only
90 'canNotCollapse' => TRUE, // An "additional palette" can not be collapsed
91 'elements' => array(
92 0 => array(
93 'type' => 'single',
94 'fieldName' => 'element4',
95 'fieldLabel' => 'element4',
96 'fieldHtml' => 'element4',
97 ),
98 1 => array(
99 'type' => 'linebreak',
100 ),
101 2 => array(
102 'type' => 'single',
103 'fieldName' => 'element5',
104 'fieldLabel' => 'element5',
105 'fieldHtml' => 'element5',
106 ),
107 ),
108 ),
109 );
110 */
111
112 // Create an intermediate structure of rendered sub elements and elements nested in palettes
113 $targetStructure = array();
114 $mainStructureCounter = -1;
115 $fieldsArray = $this->globalOptions['fieldsArray'];
116 $this->resultArray = $this->initializeResultArray();
117 foreach ($fieldsArray as $fieldString) {
118 $fieldConfiguration = $this->explodeSingleFieldShowItemConfiguration($fieldString);
119 $fieldName = $fieldConfiguration['fieldName'];
120 if ($fieldName === '--palette--') {
121 $paletteElementArray = $this->createPaletteContentArray($fieldConfiguration['paletteName']);
122 if (!empty($paletteElementArray)) {
123 $mainStructureCounter ++;
124 $targetStructure[$mainStructureCounter] = array(
125 'type' => 'palette',
126 'fieldName' => $fieldConfiguration['paletteName'],
127 'fieldLabel' => $languageService->sL($fieldConfiguration['fieldLabel']),
128 'elements' => $paletteElementArray,
129 );
130 }
131 } else {
132 if (!is_array($GLOBALS['TCA'][$table]['columns'][$fieldName])) {
133 continue;
134 }
135
136 $options = $this->globalOptions;
137 $options['fieldName'] = $fieldName;
138
139 $options['renderType'] = 'singleFieldContainer';
140 /** @var NodeFactory $nodeFactory */
141 $nodeFactory = $this->globalOptions['nodeFactory'];
142 $childResultArray = $nodeFactory->create($options)->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 = str_replace('.', '_', '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->wrapSingleFieldContentWithLabelAndOuterDiv($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
262 $options['renderType'] = 'singleFieldContainer';
263 /** @var NodeFactory $nodeFactory */
264 $nodeFactory = $this->globalOptions['nodeFactory'];
265 $singleFieldContentArray = $nodeFactory->create($options)->render();
266
267 if (!empty($singleFieldContentArray['html'])) {
268 $foundRealElement = TRUE;
269 $resultStructure[] = array(
270 'type' => 'single',
271 'fieldName' => $fieldName,
272 'fieldLabel' => $this->getSingleFieldLabel($fieldName, $fieldArray['fieldLabel']),
273 'fieldHtml' => $singleFieldContentArray['html'],
274 );
275 $singleFieldContentArray['html'] = '';
276 }
277 $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $singleFieldContentArray);
278 }
279 }
280
281 if ($foundRealElement) {
282 return $resultStructure;
283 } else {
284 return array();
285 }
286 }
287
288 /**
289 * Renders inner content of single elements of a palette and wrap it as needed
290 *
291 * @param array $elementArray Array of elements
292 * @return string Wrapped content
293 */
294 protected function renderInnerPaletteContent(array $elementArray) {
295 // Group fields
296 $groupedFields = array();
297 $row = 0;
298 $lastLineWasLinebreak = TRUE;
299 foreach ($elementArray['elements'] as $element) {
300 if ($element['type'] === 'linebreak') {
301 if (!$lastLineWasLinebreak) {
302 $row++;
303 $groupedFields[$row][] = $element;
304 $row++;
305 $lastLineWasLinebreak = TRUE;
306 }
307 } else {
308 $lastLineWasLinebreak = FALSE;
309 $groupedFields[$row][] = $element;
310 }
311 }
312
313 $result = array();
314 // Process fields
315 foreach ($groupedFields as $fields) {
316 $numberOfItems = count($fields);
317 $colWidth = (int)floor(12 / $numberOfItems);
318 // Column class calculation
319 $colClass = "col-md-12";
320 $colClear = array();
321 if ($colWidth == 6) {
322 $colClass = "col-sm-6";
323 $colClear = array(
324 2 => 'visible-sm-block visible-md-block visible-lg-block',
325 );
326 } elseif ($colWidth === 4) {
327 $colClass = "col-sm-4";
328 $colClear = array(
329 3 => 'visible-sm-block visible-md-block visible-lg-block',
330 );
331 } elseif ($colWidth === 3) {
332 $colClass = "col-sm-6 col-md-3";
333 $colClear = array(
334 2 => 'visible-sm-block',
335 4 => 'visible-md-block visible-lg-block',
336 );
337 } elseif ($colWidth <= 2) {
338 $colClass = "checkbox-column col-sm-6 col-md-3 col-lg-2";
339 $colClear = array(
340 2 => 'visible-sm-block',
341 4 => 'visible-md-block',
342 6 => 'visible-lg-block'
343 );
344 }
345
346 // Render fields
347 for ($counter = 0; $counter < $numberOfItems; $counter++) {
348 $element = $fields[$counter];
349 if ($element['type'] === 'linebreak') {
350 if ($counter !== $numberOfItems) {
351 $result[] = '<div class="clearfix"></div>';
352 }
353 } else {
354 $result[] = $this->wrapSingleFieldContentWithLabelAndOuterDiv($element, array($colClass));
355
356 // Breakpoints
357 if ($counter + 1 < $numberOfItems && !empty($colClear)) {
358 foreach ($colClear as $rowBreakAfter => $clearClass) {
359 if (($counter + 1) % $rowBreakAfter === 0) {
360 $result[] = '<div class="clearfix '. $clearClass . '"></div>';
361 }
362 }
363 }
364 }
365 }
366 }
367
368 return implode(LF, $result);
369 }
370
371 /**
372 * Add a "collapsible" button around given content
373 *
374 * @param string $elementHtml HTML of handled palette content
375 * @param string $cssId A css id to be added
376 * @return string Wrapped content
377 */
378 protected function wrapPaletteWithCollapseButton($elementHtml, $cssId) {
379 $content = array();
380 $content[] = '<p>';
381 $content[] = '<button class="btn btn-default" type="button" data-toggle="collapse" data-target="#' . $cssId . '" aria-expanded="false" aria-controls="' . $cssId . '">';
382 $content[] = IconUtility::getSpriteIcon('actions-system-options-view');
383 $content[] = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.moreOptions'));
384 $content[] = '</button>';
385 $content[] = '</p>';
386 $content[] = '<div id="' . $cssId . '" class="form-section-collapse collapse">';
387 $content[] = '<div class="row">' . $elementHtml . '</div>';
388 $content[] = '</div>';
389 return implode(LF, $content);
390 }
391
392 /**
393 * Wrap content in a field set
394 *
395 * @param string $content Incoming content
396 * @param bool $paletteHidden TRUE if the palette is hidden
397 * @param string $label Given label
398 * @return string Wrapped content
399 */
400 protected function fieldSetWrap($content, $paletteHidden = FALSE, $label = '') {
401 $fieldSetClass = 'form-section';
402 if ($paletteHidden) {
403 $fieldSetClass = 'hide';
404 }
405
406 $result = array();
407 $result[] = '<fieldset class="' . $fieldSetClass . '">';
408
409 if (!empty($label)) {
410 $result[] = '<h4 class="form-section-headline">' . htmlspecialchars($label) . '</h4>';
411 }
412
413 $result[] = $content;
414 $result[] = '</fieldset>';
415 return implode(LF, $result);
416 }
417
418 /**
419 * Wrap a single element
420 *
421 * @param array $element Given element as documented above
422 * @param array $additionalPaletteClasses Additional classes to be added to HTML
423 * @return string Wrapped element
424 */
425 protected function wrapSingleFieldContentWithLabelAndOuterDiv(array $element, array $additionalPaletteClasses = array()) {
426 $fieldName = $element['fieldName'];
427
428 $paletteFieldClasses = array(
429 'form-group',
430 't3js-formengine-palette-field',
431 );
432 foreach ($additionalPaletteClasses as $class) {
433 $paletteFieldClasses[] = $class;
434 }
435
436 $label = BackendUtility::wrapInHelp($this->globalOptions['table'], $fieldName, htmlspecialchars($element['fieldLabel']));
437
438 $content = array();
439 $content[] = '<div class="' . implode(' ', $paletteFieldClasses) . '">';
440 $content[] = '<label class="t3js-formengine-label">';
441 $content[] = $label;
442 $content[] = '<img name="req_' . $this->globalOptions['table'] . '_' . $this->globalOptions['databaseRow']['uid'] . '_' . $fieldName . '" src="clear.gif" class="t3js-formengine-field-required" alt="" />';
443 $content[] = '</label>';
444 $content[] = $element['fieldHtml'];
445 $content[] = '</div>';
446
447 return implode(LF, $content);
448 }
449
450 /**
451 * Determine label of a single field (not a palette label)
452 *
453 * @param string $fieldName The field name to calculate the label for
454 * @param string $labelFromShowItem Given label, typically from show item configuration
455 * @return string Field label
456 */
457 protected function getSingleFieldLabel($fieldName, $labelFromShowItem) {
458 $languageService = $this->getLanguageService();
459 $table = $this->globalOptions['table'];
460 $label = $labelFromShowItem;
461 if (!empty($GLOBALS['TCA'][$table]['columns'][$fieldName]['label'])) {
462 $label = $GLOBALS['TCA'][$table]['columns'][$fieldName]['label'];
463 }
464 if (!empty($labelFromShowItem)) {
465 $label = $labelFromShowItem;
466 }
467 $fieldTSConfig = FormEngineUtility::getTSconfigForTableRow($table, $this->globalOptions['databaseRow'], $fieldName);
468 if (!empty($fieldTSConfig['label'])) {
469 $label = $fieldTSConfig['label'];
470 }
471 if (!empty($fieldTSConfig['label.'][$languageService->lang])) {
472 $label = $fieldTSConfig['label.'][$languageService->lang];
473 }
474 return $languageService->sL($label);
475 }
476
477 /**
478 * TRUE if field is of type user and to wrapping is requested
479 *
480 * @param array $element Current element from "target structure" array
481 * @return boolean TRUE if user and noTableWrapping is set
482 */
483 protected function isUserNoTableWrappingField($element) {
484 $table = $this->globalOptions['table'];
485 $fieldName = $element['fieldName'];
486 if (
487 $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['type'] === 'user'
488 && !empty($GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['noTableWrapping'])
489 ) {
490 return TRUE;
491 }
492 return FALSE;
493 }
494
495 /**
496 * @return LanguageService
497 */
498 protected function getLanguageService() {
499 return $GLOBALS['LANG'];
500 }
501
502 }