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