[BUGFIX] Correctly store values of select fields
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / Element / SelectElement.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form\Element;
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\ArrayUtility;
18 use TYPO3\CMS\Core\Utility\GeneralUtility;
19 use TYPO3\CMS\Core\Utility\MathUtility;
20 use TYPO3\CMS\Backend\Utility\IconUtility;
21 use TYPO3\CMS\Backend\Utility\BackendUtility;
22
23 /**
24 * Generation of TCEform elements of the type "select"
25 */
26 class SelectElement extends AbstractFormElement {
27
28 /**
29 * If this value is set during traversal and the traversal chain can
30 * not be walked to the end this value will be returned instead.
31 *
32 * @var string
33 */
34 protected $alternativeFieldValue;
35
36 /**
37 * If this is TRUE the alternative field value will be used even if
38 * the detected field value is not empty.
39 *
40 * @var bool
41 */
42 protected $forceAlternativeFieldValueUse = FALSE;
43
44 /**
45 * The row data of the record that is currently traversed.
46 *
47 * @var array
48 */
49 protected $currentRow;
50
51 /**
52 * Name of the table that is currently traversed.
53 *
54 * @var string
55 */
56 protected $currentTable;
57
58 /**
59 * This will render a selector box element, or possibly a special construction with two selector boxes.
60 * That depends on configuration.
61 *
62 * @param string $table The table name of the record
63 * @param string $field The field name which this element is supposed to edit
64 * @param array $row The record data array where the value(s) for the field can be found
65 * @param array $additionalInformation An array with additional configuration options.
66 * @return string The HTML code for the TCEform field
67 */
68 public function render($table, $field, $row, &$additionalInformation) {
69 // Field configuration from TCA:
70 $config = $additionalInformation['fieldConf']['config'];
71 $disabled = '';
72 if ($this->isRenderReadonly() || $config['readOnly']) {
73 $disabled = ' disabled="disabled"';
74 }
75 // "Extra" configuration; Returns configuration for the field based on settings found in the "types" fieldlist.
76 $specConf = BackendUtility::getSpecConfParts($additionalInformation['extra'], $additionalInformation['fieldConf']['defaultExtras']);
77 $selItems = $this->getSelectItems($table, $field, $row, $additionalInformation);
78
79 // Creating the label for the "No Matching Value" entry.
80 $nMV_label = isset($additionalInformation['fieldTSConfig']['noMatchingValue_label'])
81 ? $this->getLanguageService()->sL($additionalInformation['fieldTSConfig']['noMatchingValue_label'])
82 : '[ ' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.noMatchingValue') . ' ]';
83 // Prepare some values:
84 $maxitems = (int)$config['maxitems'];
85 // If a SINGLE selector box...
86 if ($maxitems <= 1 && $config['renderMode'] !== 'tree') {
87 $item = $this->getSingleField_typeSelect_single($table, $field, $row, $additionalInformation, $config, $selItems, $nMV_label);
88 } elseif ($config['renderMode'] === 'checkbox') {
89 // Checkbox renderMode
90 $item = $this->getSingleField_typeSelect_checkbox($table, $field, $row, $additionalInformation, $config, $selItems, $nMV_label);
91 } elseif ($config['renderMode'] === 'singlebox') {
92 // Single selector box renderMode
93 $item = $this->getSingleField_typeSelect_singlebox($table, $field, $row, $additionalInformation, $config, $selItems, $nMV_label);
94 } elseif ($config['renderMode'] === 'tree') {
95 // Tree renderMode
96 $treeClass = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\Element\TreeElement::class, $this->formEngine);
97 $item = $treeClass->renderField($table, $field, $row, $additionalInformation, $config, $selItems, $nMV_label);
98 // Register the required number of elements
99 $minitems = MathUtility::forceIntegerInRange($config['minitems'], 0);
100 $this->formEngine->registerRequiredProperty('range', $additionalInformation['itemFormElName'], array($minitems, $maxitems, 'imgName' => $table . '_' . $row['uid'] . '_' . $field));
101 } else {
102 // Traditional multiple selector box:
103 $item = $this->getSingleField_typeSelect_multiple($table, $field, $row, $additionalInformation, $config, $selItems, $nMV_label);
104 }
105 // Wizards:
106 if (!$disabled) {
107 $altItem = '<input type="hidden" name="' . $additionalInformation['itemFormElName'] . '" value="' . htmlspecialchars($additionalInformation['itemFormElValue']) . '" />';
108 $item = $this->formEngine->renderWizards(array($item, $altItem), $config['wizards'], $table, $row, $field, $additionalInformation, $additionalInformation['itemFormElName'], $specConf);
109 }
110 return $item;
111 }
112
113 /**
114 * Creates a multiple-selector box (two boxes, side-by-side)
115 * (Render function for getSingleField_typeSelect())
116 *
117 * @param string $table See getSingleField_typeSelect()
118 * @param string $field See getSingleField_typeSelect()
119 * @param array $row See getSingleField_typeSelect()
120 * @param array $PA See getSingleField_typeSelect()
121 * @param array $config (Redundant) content of $PA['fieldConf']['config'] (for convenience)
122 * @param array $selItems Items available for selection
123 * @param string $nMV_label Label for no-matching-value
124 * @return string The HTML code for the item
125 * @see getSingleField_typeSelect()
126 */
127 public function getSingleField_typeSelect_multiple($table, $field, $row, &$PA, $config, $selItems, $nMV_label) {
128 $languageService = $this->getLanguageService();
129 $item = '';
130 $disabled = '';
131 if ($this->isRenderReadonly() || $config['readOnly']) {
132 $disabled = ' disabled="disabled"';
133 }
134 // Setting this hidden field (as a flag that JavaScript can read out)
135 if (!$disabled) {
136 $item .= '<input type="hidden" name="' . $PA['itemFormElName'] . '_mul" value="' . ($config['multiple'] ? 1 : 0) . '" />';
137 }
138 // Set max and min items:
139 $maxitems = MathUtility::forceIntegerInRange($config['maxitems'], 0);
140 if (!$maxitems) {
141 $maxitems = 100000;
142 }
143 $minitems = MathUtility::forceIntegerInRange($config['minitems'], 0);
144 // Register the required number of elements:
145 $this->formEngine->registerRequiredProperty('range', $PA['itemFormElName'], array($minitems, $maxitems, 'imgName' => $table . '_' . $row['uid'] . '_' . $field));
146 // Get "removeItems":
147 $removeItems = GeneralUtility::trimExplode(',', $PA['fieldTSConfig']['removeItems'], TRUE);
148 // Get the array with selected items:
149 $itemArray = GeneralUtility::trimExplode(',', $PA['itemFormElValue'], TRUE);
150
151 // Possibly filter some items:
152 $itemArray = ArrayUtility::keepItemsInArray(
153 $itemArray,
154 $PA['fieldTSConfig']['keepItems'],
155 function ($value) {
156 $parts = explode('|', $value, 2);
157 return rawurldecode($parts[0]);
158 }
159 );
160
161 // Perform modification of the selected items array:
162 foreach ($itemArray as $tk => $tv) {
163 $tvP = explode('|', $tv, 2);
164 $evalValue = $tvP[0];
165 $isRemoved = in_array($evalValue, $removeItems)
166 || $config['form_type'] == 'select' && $config['authMode']
167 && !$this->getBackendUserAuthentication()->checkAuthMode($table, $field, $evalValue, $config['authMode']);
168 if ($isRemoved && !$PA['fieldTSConfig']['disableNoMatchingValueElement'] && !$config['disableNoMatchingValueElement']) {
169 $tvP[1] = rawurlencode(@sprintf($nMV_label, $evalValue));
170 } else {
171 if (isset($PA['fieldTSConfig']['altLabels.'][$evalValue])) {
172 $tvP[1] = rawurlencode($languageService->sL($PA['fieldTSConfig']['altLabels.'][$evalValue]));
173 }
174 if (isset($PA['fieldTSConfig']['altIcons.'][$evalValue])) {
175 $tvP[2] = $PA['fieldTSConfig']['altIcons.'][$evalValue];
176 }
177 }
178 if ($tvP[1] == '') {
179 // Case: flexform, default values supplied, no label provided (bug #9795)
180 foreach ($selItems as $selItem) {
181 if ($selItem[1] == $tvP[0]) {
182 $tvP[1] = html_entity_decode($selItem[0]);
183 break;
184 }
185 }
186 }
187 $itemArray[$tk] = implode('|', $tvP);
188 }
189 $itemsToSelect = '';
190 $filterTextfield = '';
191 $filterSelectbox = '';
192 $size = 0;
193 if (!$disabled) {
194 // Create option tags:
195 $opt = array();
196 $styleAttrValue = '';
197 foreach ($selItems as $p) {
198 if ($config['iconsInOptionTags']) {
199 $styleAttrValue = $this->formEngine->optionTagStyle($p[2]);
200 }
201 $opt[] = '<option value="' . htmlspecialchars($p[1]) . '"'
202 . ($styleAttrValue ? ' style="' . htmlspecialchars($styleAttrValue) . '"' : '')
203 . ' title="' . $p[0] . '">' . $p[0] . '</option>';
204 }
205 // Put together the selector box:
206 $selector_itemListStyle = isset($config['itemListStyle'])
207 ? ' style="' . htmlspecialchars($config['itemListStyle']) . '"'
208 : ' style="' . $this->formEngine->defaultMultipleSelectorStyle . '"';
209 $size = (int)$config['size'];
210 $size = $config['autoSizeMax']
211 ? MathUtility::forceIntegerInRange(count($itemArray) + 1, MathUtility::forceIntegerInRange($size, 1), $config['autoSizeMax'])
212 : $size;
213 $sOnChange = implode('', $PA['fieldChangeFunc']);
214
215 $multiSelectId = str_replace('.', '', uniqid('tceforms-multiselect-', TRUE));
216 $itemsToSelect = '
217 <select data-relatedfieldname="' . htmlspecialchars($PA['itemFormElName']) . '" data-exclusivevalues="'
218 . htmlspecialchars($config['exclusiveKeys']) . '" id="' . $multiSelectId . '" name="' . $PA['itemFormElName'] . '_sel" '
219 . ' class="form-control t3js-formengine-select-itemstoselect" '
220 . ($size ? ' size="' . $size . '"' : '') . ' onchange="' . htmlspecialchars($sOnChange) . '"'
221 . $PA['onFocus'] . $selector_itemListStyle . '>
222 ' . implode('
223 ', $opt) . '
224 </select>';
225
226 // enable filter functionality via a text field
227 if ($config['enableMultiSelectFilterTextfield']) {
228 $filterTextfield = '
229 <span class="input-group input-group-sm">
230 <span class="input-group-addon">
231 <span class="fa fa-filter"></span>
232 </span>
233 <input class="t3js-formengine-multiselect-filter-textfield form-control" value="" />
234 </span>';
235 }
236
237 // enable filter functionality via a select
238 if (isset($config['multiSelectFilterItems']) && is_array($config['multiSelectFilterItems']) && count($config['multiSelectFilterItems']) > 1) {
239 $filterDropDownOptions = array();
240 foreach ($config['multiSelectFilterItems'] as $optionElement) {
241 $optionValue = $languageService->sL(isset($optionElement[1]) && $optionElement[1] != '' ? $optionElement[1]
242 : $optionElement[0]);
243 $filterDropDownOptions[] = '<option value="' . htmlspecialchars($languageService->sL($optionElement[0])) . '">'
244 . htmlspecialchars($optionValue) . '</option>';
245 }
246 $filterSelectbox = '<select class="form-control input-sm t3js-formengine-multiselect-filter-dropdown">
247 ' . implode('
248 ', $filterDropDownOptions) . '
249 </select>';
250 }
251 }
252
253 if (!empty(trim($filterSelectbox)) && !empty(trim($filterTextfield))) {
254 $filterSelectbox = '<div class="form-multigroup-item form-multigroup-element">' . $filterSelectbox . '</div>';
255 $filterTextfield = '<div class="form-multigroup-item form-multigroup-element">' . $filterTextfield . '</div>';
256 $selectBoxFilterContents = '<div class="t3js-formengine-multiselect-filter-container form-multigroup-wrap">' . $filterSelectbox . $filterTextfield . '</div>';
257 } else {
258 $selectBoxFilterContents = trim($filterSelectbox . ' ' . $filterTextfield);
259 }
260
261 // Pass to "dbFileIcons" function:
262 $params = array(
263 'size' => $size,
264 'autoSizeMax' => MathUtility::forceIntegerInRange($config['autoSizeMax'], 0),
265 'style' => isset($config['selectedListStyle'])
266 ? ' style="' . htmlspecialchars($config['selectedListStyle']) . '"'
267 : ' style="' . $this->formEngine->defaultMultipleSelectorStyle . '"',
268 'dontShowMoveIcons' => $maxitems <= 1,
269 'maxitems' => $maxitems,
270 'info' => '',
271 'headers' => array(
272 'selector' => $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.selected'),
273 'items' => $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.items'),
274 'selectorbox' => $selectBoxFilterContents,
275 ),
276 'noBrowser' => 1,
277 'rightbox' => $itemsToSelect,
278 'readOnly' => $disabled
279 );
280 $item .= $this->formEngine->dbFileIcons($PA['itemFormElName'], '', '', $itemArray, '', $params, $PA['onFocus']);
281 return $item;
282 }
283
284 /**
285 * Collects the items for a select field by reading the configured
286 * select items from the configuration and / or by collecting them
287 * from a foreign table.
288 *
289 * @param string $table The table name of the record
290 * @param string $fieldName The select field name
291 * @param array $row The record data array where the value(s) for the field can be found
292 * @param array $PA An array with additional configuration options.
293 * @return array
294 */
295 protected function getSelectItems($table, $fieldName, array $row, array $PA) {
296 $config = $PA['fieldConf']['config'];
297
298 // Getting the selector box items from the system
299 $selectItems = $this->formEngine->addSelectOptionsToItemArray(
300 $this->formEngine->initItemArray($PA['fieldConf']),
301 $PA['fieldConf'],
302 $this->formEngine->setTSconfig($table, $row),
303 $fieldName
304 );
305
306 // Possibly filter some items:
307 $selectItems = ArrayUtility::keepItemsInArray(
308 $selectItems,
309 $PA['fieldTSConfig']['keepItems'],
310 function ($value) {
311 return $value[1];
312 }
313 );
314
315 // Possibly add some items:
316 $selectItems = $this->formEngine->addItems($selectItems, $PA['fieldTSConfig']['addItems.']);
317
318 // Process items by a user function:
319 if (isset($config['itemsProcFunc']) && $config['itemsProcFunc']) {
320 $selectItems = $this->formEngine->procItems($selectItems, $PA['fieldTSConfig']['itemsProcFunc.'], $config, $table, $row, $fieldName);
321 }
322
323 // Possibly remove some items:
324 $removeItems = GeneralUtility::trimExplode(',', $PA['fieldTSConfig']['removeItems'], TRUE);
325 foreach ($selectItems as $selectItemIndex => $selectItem) {
326
327 // Checking languages and authMode:
328 $languageDeny = FALSE;
329 $beUserAuth = $this->getBackendUserAuthentication();
330 if (
331 !empty($GLOBALS['TCA'][$table]['ctrl']['languageField'])
332 && $GLOBALS['TCA'][$table]['ctrl']['languageField'] === $fieldName
333 && !$beUserAuth->checkLanguageAccess($selectItem[1])
334 ) {
335 $languageDeny = TRUE;
336 }
337
338 $authModeDeny = FALSE;
339 if (
340 ($config['form_type'] === 'select')
341 && $config['authMode']
342 && !$beUserAuth->checkAuthMode($table, $fieldName, $selectItem[1], $config['authMode'])
343 ) {
344 $authModeDeny = TRUE;
345 }
346
347 if (in_array($selectItem[1], $removeItems) || $languageDeny || $authModeDeny) {
348 unset($selectItems[$selectItemIndex]);
349 } elseif (isset($PA['fieldTSConfig']['altLabels.'][$selectItem[1]])) {
350 $selectItems[$selectItemIndex][0] = htmlspecialchars($this->getLanguageService()->sL($PA['fieldTSConfig']['altLabels.'][$selectItem[1]]));
351 }
352
353 // Removing doktypes with no access:
354 if (($table === 'pages' || $table === 'pages_language_overlay') && $fieldName === 'doktype') {
355 if (!($beUserAuth->isAdmin() || GeneralUtility::inList($beUserAuth->groupData['pagetypes_select'], $selectItem[1]))) {
356 unset($selectItems[$selectItemIndex]);
357 }
358 }
359 }
360
361 return $selectItems;
362 }
363
364 /**
365 * Creates a single-selector box
366 * (Render function for getSingleField_typeSelect())
367 *
368 * @param string $table See getSingleField_typeSelect()
369 * @param string $field See getSingleField_typeSelect()
370 * @param array $row See getSingleField_typeSelect()
371 * @param array $PA See getSingleField_typeSelect()
372 * @param array $config (Redundant) content of $PA['fieldConf']['config'] (for convenience)
373 * @param array $selectItems Items available for selection
374 * @param string $noMatchingLabel Label for no-matching-value
375 * @return string The HTML code for the item
376 * @see getSingleField_typeSelect()
377 */
378 public function getSingleField_typeSelect_single($table, $field, $row, &$PA, $config, $selectItems, $noMatchingLabel) {
379
380 // check against inline uniqueness
381 $inlineParent = $this->formEngine->inline->getStructureLevel(-1);
382 $uniqueIds = NULL;
383 if (is_array($inlineParent) && $inlineParent['uid']) {
384 if ($inlineParent['config']['foreign_table'] == $table && $inlineParent['config']['foreign_unique'] == $field) {
385 $uniqueIds = $this->formEngine->inline->inlineData['unique'][$this->formEngine->inline->inlineNames['object']
386 . InlineElement::Structure_Separator . $table]['used'];
387 $PA['fieldChangeFunc']['inlineUnique'] = 'inline.updateUnique(this,\'' . $this->formEngine->inline->inlineNames['object']
388 . InlineElement::Structure_Separator . $table . '\',\'' . $this->formEngine->inline->inlineNames['form']
389 . '\',\'' . $row['uid'] . '\');';
390 }
391 // hide uid of parent record for symmetric relations
392 if (
393 $inlineParent['config']['foreign_table'] == $table
394 && ($inlineParent['config']['foreign_field'] == $field || $inlineParent['config']['symmetric_field'] == $field)
395 ) {
396 $uniqueIds[] = $inlineParent['uid'];
397 }
398 }
399
400 // Initialization:
401 $selectId = str_replace('.', '', uniqid('tceforms-select-', TRUE));
402 $selectedIndex = 0;
403 $selectedIcon = '';
404 $noMatchingValue = 1;
405 $onlySelectedIconShown = 0;
406 $size = (int)$config['size'];
407
408 // Style set on <select/>
409 $out = '';
410 $options = '';
411 $disabled = FALSE;
412 if ($this->isRenderReadonly() || $config['readOnly']) {
413 $disabled = TRUE;
414 $onlySelectedIconShown = 1;
415 }
416 // Register as required if minitems is greater than zero
417 if (($minItems = MathUtility::forceIntegerInRange($config['minitems'], 0)) > 0) {
418 $this->formEngine->registerRequiredProperty('field', $table . '_' . $row['uid'] . '_' . $field, $PA['itemFormElName']);
419 }
420
421 // Icon configuration:
422 if ($config['suppress_icons'] == 'IF_VALUE_FALSE') {
423 $suppressIcons = !$PA['itemFormElValue'] ? 1 : 0;
424 } elseif ($config['suppress_icons'] == 'ONLY_SELECTED') {
425 $suppressIcons = 0;
426 $onlySelectedIconShown = 1;
427 } elseif ($config['suppress_icons']) {
428 $suppressIcons = 1;
429 } else {
430 $suppressIcons = 0;
431 }
432
433 // Prepare groups
434 $selectItemCounter = 0;
435 $selectItemGroupCount = 0;
436 $selectItemGroups = array();
437 $selectIcons = array();
438 $selectedValue = '';
439 foreach ($selectItems as $item) {
440 if ($item[1] === '--div--') {
441 // IS OPTGROUP
442 if ($selectItemCounter !== 0) {
443 $selectItemGroupCount++;
444 }
445 $selectItemGroups[$selectItemGroupCount]['header'] = array(
446 'title' => $item[0],
447 'icon' => (!empty($item[2]) ? $this->formEngine->getIconHtml($item[2]) : '')
448 );
449 } else {
450 // IS ITEM
451 $title = htmlspecialchars($item['0'], ENT_COMPAT, 'UTF-8', FALSE);
452 $icon = (!empty($item[2]) ? $this->formEngine->getIconHtml($item[2], $title, $title) : '');
453 $selected = ((string)$PA['itemFormElValue'] === (string)$item[1] ? 1 : 0);
454 if ($selected) {
455 $selectedIndex = $selectItemCounter;
456 $selectedValue = $item[1];
457 $selectedIcon = $icon;
458 $noMatchingValue = 0;
459 }
460 $selectItemGroups[$selectItemGroupCount]['items'][] = array(
461 'title' => $title,
462 'value' => $item[1],
463 'icon' => $icon,
464 'selected' => $selected,
465 'index' => $selectItemCounter
466 );
467 // ICON
468 if ($icon && !$suppressIcons && (!$onlySelectedIconShown || $selected)) {
469 $onClick = $this->formEngine->elName($PA['itemFormElName']) . '.selectedIndex=' . $selectItemCounter . ';';
470 if ($config['iconsInOptionTags']) {
471 $onClick .= 'document.getElementById(\'' . $selectId . '_icon\').innerHTML = '
472 . $this->formEngine->elName($PA['itemFormElName'])
473 . '.options[' . $selectItemCounter . '].getAttribute(\'data-icon\'); ';
474 }
475 $onClick .= implode('', $PA['fieldChangeFunc']);
476 $onClick .= 'this.blur();return false;';
477 $selectIcons[] = array(
478 'title' => $title,
479 'icon' => $icon,
480 'index' => $selectItemCounter,
481 'onClick' => $onClick
482 );
483 }
484 $selectItemCounter++;
485 }
486
487 }
488
489 // No-matching-value:
490 if ($PA['itemFormElValue'] && $noMatchingValue && !$PA['fieldTSConfig']['disableNoMatchingValueElement'] && !$config['disableNoMatchingValueElement']) {
491 $noMatchingLabel = @sprintf($noMatchingLabel, $PA['itemFormElValue']);
492 $options = '<option value="' . htmlspecialchars($PA['itemFormElValue']) . '" selected="selected">' . htmlspecialchars($noMatchingLabel) . '</option>';
493 } elseif (!$selectedIcon && $selectItemGroups[0]['items'][0]['icon']) {
494 $selectedIcon = $selectItemGroups[0]['items'][0]['icon'];
495 }
496
497 // Process groups
498 foreach ($selectItemGroups as $selectItemGroup) {
499 $optionGroup = is_array($selectItemGroup['header']);
500 $options .= ($optionGroup ? '<optgroup label="' . htmlspecialchars($selectItemGroup['header']['title'], ENT_COMPAT, 'UTF-8', FALSE) . '">' : '');
501 if (is_array($selectItemGroup['items'])) {
502 foreach ($selectItemGroup['items'] as $item) {
503 $options .= '<option value="' . htmlspecialchars($item['value']) . '" data-icon="' .
504 htmlspecialchars($item['icon']) . '"'
505 . ($item['selected'] ? ' selected="selected"' : '') . '>' . $item['title'] . '</option>';
506 }
507 }
508 $options .= ($optionGroup ? '</optgroup>' : '');
509 }
510
511 // Create item form fields:
512 $sOnChange = 'if (this.options[this.selectedIndex].value==\'--div--\') {this.selectedIndex=' . $selectedIndex . ';} ';
513 if ($config['iconsInOptionTags']) {
514 $sOnChange .= 'document.getElementById(\'' . $selectId . '_icon\').innerHTML = this.options[this.selectedIndex].getAttribute(\'data-icon\'); ';
515 }
516 $sOnChange .= implode('', $PA['fieldChangeFunc']);
517
518 // Add icons in option tags
519 $prepend = '';
520 $append = '';
521 if ($config['iconsInOptionTags']) {
522 $prepend = '<div class="input-group"><div id="' . $selectId . '_icon" class="input-group-addon input-group-icon t3js-formengine-select-prepend">' . $selectedIcon . '</div>';
523 $append = '</div>';
524 }
525
526 // Build the element
527 $out .= '
528 <div class="form-control-wrap">
529 ' . $prepend . '
530 <select'
531 . ' id="' . $selectId . '"'
532 . ' name="' . $PA['itemFormElName'] . '"'
533 . ' class="form-control form-control-adapt"'
534 . ($size ? ' size="' . $size . '"' : '')
535 . ' onchange="' . htmlspecialchars($sOnChange) . '"'
536 . $PA['onFocus']
537 . ($disabled ? ' disabled="disabled"' : '')
538 . '>
539 ' . $options . '
540 </select>
541 ' . $append . '
542 </div>';
543
544 // Create icon table:
545 if (count($selectIcons) && !$config['noIconsBelowSelect']) {
546 $selectIconColumns = (int)$config['selicon_cols'];
547 if (!$selectIconColumns) {
548 $selectIconColumns = count($selectIcons);
549 }
550 $selectIconColumns = ($selectIconColumns > 12 ? 12 : $selectIconColumns);
551 $selectIconRows = ceil(count($selectIcons) / $selectIconColumns);
552 $selectIcons = array_pad($selectIcons, $selectIconRows * $selectIconColumns, '');
553 $out .= '<div class="table-fit table-fit-inline-block"><table class="table table-condensed table-white table-center"><tbody><tr>';
554 for ($selectIconCount = 0; $selectIconCount < count($selectIcons); $selectIconCount++) {
555 if ($selectIconCount % $selectIconColumns === 0 && $selectIconCount !== 0) {
556 $out .= '</tr><tr>';
557 }
558 $out .= '<td>';
559 if (is_array($selectIcons[$selectIconCount])) {
560 $out .= (!$onlySelectedIconShown ? '<a href="#" title="' . $selectIcons[$selectIconCount]['title'] . '" onClick="' . htmlspecialchars($selectIcons[$selectIconCount]['onClick']) . '">' : '');
561 $out .= $selectIcons[$selectIconCount]['icon'];
562 $out .= (!$onlySelectedIconShown ? '</a>' : '');
563 }
564 $out . '</td>';
565 }
566 $out .= '</tr></tbody></table></div>';
567 }
568
569 return $out;
570 }
571
572 /**
573 * Creates a checkbox list (renderMode = "checkbox")
574 * (Render function for getSingleField_typeSelect())
575 *
576 * @param string $table See getSingleField_typeSelect()
577 * @param string $field See getSingleField_typeSelect()
578 * @param array $row See getSingleField_typeSelect()
579 * @param array $PA See getSingleField_typeSelect()
580 * @param array $config (Redundant) content of $PA['fieldConf']['config'] (for convenience)
581 * @param array $selItems Items available for selection
582 * @param string $nMV_label Label for no-matching-value
583 * @return string The HTML code for the item
584 * @see getSingleField_typeSelect()
585 */
586 public function getSingleField_typeSelect_checkbox($table, $field, $row, &$PA, $config, $selItems, $nMV_label) {
587 if (empty($selItems)) {
588 return '';
589 }
590 // Get values in an array (and make unique, which is fine because there can be no duplicates anyway):
591 $itemArray = array_flip($this->formEngine->extractValuesOnlyFromValueLabelList($PA['itemFormElValue']));
592 $output = '';
593
594 // Disabled
595 $disabled = 0;
596 if ($this->isRenderReadonly() || $config['readOnly']) {
597 $disabled = 1;
598 }
599 // Traverse the Array of selector box items:
600 $groups = array();
601 $currentGroup = 0;
602 $c = 0;
603 $sOnChange = '';
604 if (!$disabled) {
605 $sOnChange = implode('', $PA['fieldChangeFunc']);
606 // Used to accumulate the JS needed to restore the original selection.
607 foreach ($selItems as $p) {
608 // Non-selectable element:
609 if ($p[1] === '--div--') {
610 $selIcon = '';
611 if (isset($p[2]) && $p[2] != 'empty-emtpy') {
612 $selIcon = $this->formEngine->getIconHtml($p[2]);
613 }
614 $currentGroup++;
615 $groups[$currentGroup]['header'] = array(
616 'icon' => $selIcon,
617 'title' => htmlspecialchars($p[0])
618 );
619 } else {
620
621 // Check if some help text is available
622 // Since TYPO3 4.5 help text is expected to be an associative array
623 // with two key, "title" and "description"
624 // For the sake of backwards compatibility, we test if the help text
625 // is a string and use it as a description (this could happen if items
626 // are modified with an itemProcFunc)
627 $hasHelp = FALSE;
628 $help = '';
629 $helpArray = array();
630 if (is_array($p[3]) && count($p[3]) > 0 || !empty($p[3])) {
631 $hasHelp = TRUE;
632 if (is_array($p[3])) {
633 $helpArray = $p[3];
634 } else {
635 $helpArray['description'] = $p[3];
636 }
637 }
638 if ($hasHelp) {
639 $help = BackendUtility::wrapInHelp('', '', '', $helpArray);
640 }
641
642 // Selected or not by default:
643 $checked = 0;
644 if (isset($itemArray[$p[1]])) {
645 $checked = 1;
646 unset($itemArray[$p[1]]);
647 }
648
649 // Build item array
650 $groups[$currentGroup]['items'][] = array(
651 'id' => str_replace('.', '', uniqid('select_checkbox_row_', TRUE)),
652 'name' => $PA['itemFormElName'] . '[' . $c . ']',
653 'value' => $p[1],
654 'checked' => $checked,
655 'disabled' => $disabled,
656 'class' => '',
657 'icon' => (!empty($p[2]) ? $this->formEngine->getIconHtml($p[2]) : IconUtility::getSpriteIcon('empty-empty')),
658 'title' => htmlspecialchars($p[0], ENT_COMPAT, 'UTF-8', FALSE),
659 'help' => $help
660 );
661 $c++;
662 }
663 }
664 }
665 // Remaining values (invalid):
666 if (count($itemArray) && !$PA['fieldTSConfig']['disableNoMatchingValueElement'] && !$config['disableNoMatchingValueElement']) {
667 $currentGroup++;
668 foreach ($itemArray as $theNoMatchValue => $temp) {
669 // Build item array
670 $groups[$currentGroup]['items'][] = array(
671 'id' => str_replace('.', '', uniqid('select_checkbox_row_', TRUE)),
672 'name' => $PA['itemFormElName'] . '[' . $c . ']',
673 'value' => $theNoMatchValue,
674 'checked' => 1,
675 'disabled' => $disabled,
676 'class' => 'danger',
677 'icon' => '',
678 'title' => htmlspecialchars(@sprintf($nMV_label, $theNoMatchValue), ENT_COMPAT, 'UTF-8', FALSE),
679 'help' => ''
680 );
681 $c++;
682 }
683 }
684 // Add an empty hidden field which will send a blank value if all items are unselected.
685 $output .= '<input type="hidden" class="select-checkbox" name="' . htmlspecialchars($PA['itemFormElName']) . '" value="" />';
686
687 // Building the checkboxes
688 foreach($groups as $groupKey => $group){
689 $groupId = htmlspecialchars($PA['itemFormElID']) . '-group-' . $groupKey;
690 $output .= '<div class="panel panel-default">';
691 if(is_array($group['header'])){
692 $output .= '
693 <div class="panel-heading">
694 <a data-toggle="collapse" href="#' . $groupId . '" aria-expanded="true" aria-controls="' . $groupId . '">
695 ' . $group['header']['icon'] . '
696 ' . $group['header']['title'] . '
697 </a>
698 </div>
699 ';
700 }
701 if(is_array($group['items']) && count($group['items']) >= 1){
702 $tableRows = '';
703 $checkGroup = array();
704 $uncheckGroup = array();
705 $resetGroup = array();
706
707 // Render rows
708 foreach($group['items'] as $item){
709 $tableRows .= '
710 <tr class="' . $item['class'] . '">
711 <td class="col-checkbox">
712 <input type="checkbox"
713 id="' . $item['id'] . '"
714 name="' . htmlspecialchars($item['name']) . '"
715 value="' . htmlspecialchars($item['value']) . '"
716 onclick="' . htmlspecialchars($sOnChange) . '"
717 ' . ($item['checked'] ? ' checked=checked' : '') . '
718 ' . ($item['disabled'] ? ' disabled=disabled' : '') . '
719 ' . $PA['onFocus'] . ' />
720 </td>
721 <td class="col-icon">
722 <label class="label-block" for="' . $item['id'] . '">' . $item['icon'] . '</label>
723 </td>
724 <td class="col-title">
725 <label class="label-block" for="' . $item['id'] . '">' . $item['title'] . '</label>
726 </td>
727 <td>' . $item['help'] . '</td>
728 </tr>
729 ';
730 $checkGroup[] = $this->formEngine->elName($item['name']) . '.checked=1;';
731 $uncheckGroup[] = $this->formEngine->elName($item['name']) . '.checked=0;';
732 $resetGroup[] = $this->formEngine->elName($item['name']) . '.checked='.$item['checked'] . ';';
733 }
734
735 // Build toggle group checkbox
736 $toggleGroupCheckbox = '';
737 if(count($resetGroup)){
738 $toggleGroupCheckbox = '
739 <input type="checkbox" class="checkbox" onclick="if (checked) {' . htmlspecialchars(implode('', $checkGroup) . '} else {' . implode('', $uncheckGroup)) . '}">
740 ';
741 }
742
743 // Build reset group button
744 $resetGroupBtn = '';
745 if(count($resetGroup)){
746 $resetGroupBtn = '
747 <a href="#" class="btn btn-default" onclick="' . implode('', $resetGroup) . ' return false;' . '">
748 ' . IconUtility::getSpriteIcon('actions-edit-undo', array('title' => htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.revertSelection')))) . '
749 ' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.revertSelection') . '
750 </a>
751 ';
752 }
753
754 $output .= '
755 <div id="' . $groupId . '" class="panel-collapse collapse in" role="tabpanel">
756 <div class="table-fit">
757 <table class="table table-transparent table-hover">
758 <thead>
759 <tr>
760 <th class="col-checkbox">' . $toggleGroupCheckbox . '</th>
761 <th class="col-icon"></th>
762 <th class="text-right" colspan="2">' . $resetGroupBtn . '</th>
763 </tr>
764 </thead>
765 <tbody>' . $tableRows . '</tbody>
766 </table>
767 </div>
768 </div>
769 ';
770 }
771 $output .= '</div>';
772 }
773
774 return $output;
775 }
776
777 /**
778 * Creates a selectorbox list (renderMode = "singlebox")
779 * (Render function for getSingleField_typeSelect())
780 *
781 * @param string $table See getSingleField_typeSelect()
782 * @param string $field See getSingleField_typeSelect()
783 * @param array $row See getSingleField_typeSelect()
784 * @param array $PA See getSingleField_typeSelect()
785 * @param array $config (Redundant) content of $PA['fieldConf']['config'] (for convenience)
786 * @param array $selItems Items available for selection
787 * @param string $nMV_label Label for no-matching-value
788 * @return string The HTML code for the item
789 * @see getSingleField_typeSelect()
790 */
791 public function getSingleField_typeSelect_singlebox($table, $field, $row, &$PA, $config, $selItems, $nMV_label) {
792 $languageService = $this->getLanguageService();
793 // Get values in an array (and make unique, which is fine because there can be no duplicates anyway):
794 $itemArray = array_flip($this->formEngine->extractValuesOnlyFromValueLabelList($PA['itemFormElValue']));
795 $item = '';
796 $disabled = '';
797 if ($this->isRenderReadonly() || $config['readOnly']) {
798 $disabled = ' disabled="disabled"';
799 }
800 // Traverse the Array of selector box items:
801 $opt = array();
802 // Used to accumulate the JS needed to restore the original selection.
803 $restoreCmd = array();
804 $c = 0;
805 foreach ($selItems as $p) {
806 // Selected or not by default:
807 $sM = '';
808 if (isset($itemArray[$p[1]])) {
809 $sM = ' selected="selected"';
810 $restoreCmd[] = $this->formEngine->elName(($PA['itemFormElName'] . '[]')) . '.options[' . $c . '].selected=1;';
811 unset($itemArray[$p[1]]);
812 }
813 // Non-selectable element:
814 $nonSel = '';
815 if ((string)$p[1] === '--div--') {
816 $nonSel = ' onclick="this.selected=0;" class="formcontrol-select-divider"';
817 }
818 // Icon style for option tag:
819 $styleAttrValue = '';
820 if ($config['iconsInOptionTags']) {
821 $styleAttrValue = $this->formEngine->optionTagStyle($p[2]);
822 }
823 // Compile <option> tag:
824 $opt[] = '<option value="' . htmlspecialchars($p[1]) . '"' . $sM . $nonSel
825 . ($styleAttrValue ? ' style="' . htmlspecialchars($styleAttrValue) . '"' : '') . '>'
826 . htmlspecialchars($p[0], ENT_COMPAT, 'UTF-8', FALSE) . '</option>';
827 $c++;
828 }
829 // Remaining values:
830 if (count($itemArray) && !$PA['fieldTSConfig']['disableNoMatchingValueElement'] && !$config['disableNoMatchingValueElement']) {
831 foreach ($itemArray as $theNoMatchValue => $temp) {
832 // Compile <option> tag:
833 array_unshift($opt, '<option value="' . htmlspecialchars($theNoMatchValue) . '" selected="selected">'
834 . htmlspecialchars(@sprintf($nMV_label, $theNoMatchValue), ENT_COMPAT, 'UTF-8', FALSE) . '</option>');
835 }
836 }
837 // Compile selector box:
838 $sOnChange = implode('', $PA['fieldChangeFunc']);
839 $selector_itemListStyle = isset($config['itemListStyle'])
840 ? ' style="' . htmlspecialchars($config['itemListStyle']) . '"'
841 : ' style="' . $this->formEngine->defaultMultipleSelectorStyle . '"';
842 $size = (int)$config['size'];
843 $cssPrefix = $size === 1 ? 'tceforms-select' : 'tceforms-multiselect';
844 $size = $config['autoSizeMax']
845 ? MathUtility::forceIntegerInRange(count($selItems) + 1, MathUtility::forceIntegerInRange($size, 1), $config['autoSizeMax'])
846 : $size;
847 $selectBox = '<select id="' . str_replace('.', '', uniqid($cssPrefix, TRUE)) . '" name="' . $PA['itemFormElName'] . '[]" '
848 . 'class="form-control ' . $cssPrefix . '"' . ($size ? ' size="' . $size . '" ' : '')
849 . ' multiple="multiple" onchange="' . htmlspecialchars($sOnChange) . '"' . $PA['onFocus']
850 . ' ' . $selector_itemListStyle . $disabled . '>
851 ' . implode('
852 ', $opt) . '
853 </select>';
854 // Add an empty hidden field which will send a blank value if all items are unselected.
855 if (!$disabled) {
856 $item .= '<input type="hidden" name="' . htmlspecialchars($PA['itemFormElName']) . '" value="" />';
857 }
858 // Put it all into a table:
859 $onClick = htmlspecialchars($this->formEngine->elName(($PA['itemFormElName'] . '[]')) . '.selectedIndex=-1;' . implode('', $restoreCmd) . ' return false;');
860 $width = $this->formEngine->formMaxWidth($this->formEngine->defaultInputWidth);
861 $item .= '
862 <div class="form-control-wrap" ' . ($width ? ' style="max-width: ' . $width . 'px"' : '') . '>
863 <div class="form-wizards-wrap form-wizards-aside">
864 <div class="form-wizards-element">
865 ' . $selectBox . '
866 </div>
867 <div class="form-wizards-items">
868 <a href="#" class="btn btn-default" onclick="' . $onClick . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.revertSelection')) . '">'
869 . IconUtility::getSpriteIcon('actions-edit-undo') . '</a>
870 </div>
871 </div>
872 </div>
873 <p>
874 <em>' . htmlspecialchars($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.holdDownCTRL')) . '</em>
875 </p>
876 ';
877 return $item;
878 }
879
880 /**
881 * If the select field is build by a foreign_table the related UIDs
882 * will be returned.
883 *
884 * Otherwise the label of the currently selected value will be written
885 * to the alternativeFieldValue class property.
886 *
887 * @param array $fieldConfig The "config" section of the TCA for the current select field.
888 * @param string $fieldName The name of the select field.
889 * @param string $value The current value in the local record, usually a comma separated list of selected values.
890 * @return array Array of related UIDs.
891 */
892 public function getRelatedSelectFieldUids(array $fieldConfig, $fieldName, $value) {
893 $relatedUids = array();
894
895 $isTraversable = FALSE;
896 if (isset($fieldConfig['foreign_table'])) {
897 $isTraversable = TRUE;
898 // if a foreign_table is used we pre-filter the records for performance
899 $fieldConfig['foreign_table_where'] .= ' AND ' . $fieldConfig['foreign_table'] . '.uid IN (' . $value . ')';
900 }
901
902 $PA = array();
903 $PA['fieldConf']['config'] = $fieldConfig;
904 $PA['fieldConf']['config']['form_type'] = $PA['fieldConf']['config']['form_type'] ? $PA['fieldConf']['config']['form_type'] : $PA['fieldConf']['config']['type'];
905 $PA['fieldTSConfig'] = $this->formEngine->setTSconfig($this->currentTable, $this->currentRow, $fieldName);
906 $PA['fieldConf']['config'] = $this->formEngine->overrideFieldConf($PA['fieldConf']['config'], $PA['fieldTSConfig']);
907 $selectItemArray = $this->getSelectItems($this->currentTable, $fieldName, $this->currentRow, $PA);
908
909 if ($isTraversable && count($selectItemArray)) {
910 $this->currentTable = $fieldConfig['foreign_table'];
911 $relatedUids = $this->getSelectedValuesFromSelectItemArray($selectItemArray, $value);
912 } else {
913 $selectedLabels = $this->getSelectedValuesFromSelectItemArray($selectItemArray, $value, 1, TRUE);
914 if (count($selectedLabels) === 1) {
915 $this->alternativeFieldValue = $selectedLabels[0];
916 $this->forceAlternativeFieldValueUse = TRUE;
917 }
918 }
919
920 return $relatedUids;
921 }
922
923 /**
924 * Extracts the selected values from a given array of select items.
925 *
926 * @param array $selectItemArray The select item array generated by \TYPO3\CMS\Backend\Form\FormEngine->getSelectItems.
927 * @param string $value The currently selected value(s) as comma separated list.
928 * @param int|NULL $maxItems Optional value, if set processing is skipped and an empty array will be returned when the number of selected values is larger than the provided value.
929 * @param bool $returnLabels If TRUE the select labels will be returned instead of the values.
930 * @return array
931 */
932 protected function getSelectedValuesFromSelectItemArray(array $selectItemArray, $value, $maxItems = NULL, $returnLabels = FALSE) {
933 $values = GeneralUtility::trimExplode(',', $value);
934 $selectedValues = array();
935
936 if ($maxItems !== NULL && (count($values) > (int)$maxItems)) {
937 return $selectedValues;
938 }
939
940 foreach ($selectItemArray as $selectItem) {
941 $selectItemValue = $selectItem[1];
942 if (in_array($selectItemValue, $values)) {
943 if ($returnLabels) {
944 $selectedValues[] = $selectItem[0];
945 } else {
946 $selectedValues[] = $selectItemValue;
947 }
948 }
949 }
950
951 return $selectedValues;
952 }
953
954 /**
955 * @param string $alternativeFieldValue
956 */
957 public function setAlternativeFieldValue($alternativeFieldValue) {
958 $this->alternativeFieldValue = $alternativeFieldValue;
959 }
960
961 /**
962 * @param array $currentRow
963 */
964 public function setCurrentRow($currentRow) {
965 $this->currentRow = $currentRow;
966 }
967
968 /**
969 * @param string $currentTable
970 */
971 public function setCurrentTable($currentTable) {
972 $this->currentTable = $currentTable;
973 }
974
975 /**
976 * @param bool $forceAlternativeFieldValueUse
977 */
978 public function setForceAlternativeFieldValueUse($forceAlternativeFieldValueUse) {
979 $this->forceAlternativeFieldValueUse = $forceAlternativeFieldValueUse;
980 }
981
982 /**
983 * @return string
984 */
985 public function getAlternativeFieldValue() {
986 return $this->alternativeFieldValue;
987 }
988
989 /**
990 * @return array
991 */
992 public function getCurrentRow() {
993 return $this->currentRow;
994 }
995
996 /**
997 * @return string
998 */
999 public function getCurrentTable() {
1000 return $this->currentTable;
1001 }
1002
1003 /**
1004 * @return boolean
1005 */
1006 public function isForceAlternativeFieldValueUse() {
1007 return $this->forceAlternativeFieldValueUse;
1008 }
1009
1010 }