[BUGFIX] Correct inline foreign_unique with target type="group" handling
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / Element / SelectSingleElement.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\Backend\Form\InlineStackProcessor;
18 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20 use TYPO3\CMS\Core\Utility\StringUtility;
21
22 /**
23 * Creates a widget where only one item can be selected.
24 * This is either a select drop-down if no size config is given or set to 1, or a select box.
25 *
26 * This is rendered for type=select, renderType=selectSingle
27 */
28 class SelectSingleElement extends AbstractFormElement
29 {
30 /**
31 * Default field wizards enabled for this element.
32 *
33 * @var array
34 */
35 protected $defaultFieldWizard = [
36 'selectIcons' => [
37 'renderType' => 'selectIcons',
38 'disabled' => true,
39 ],
40 'localizationStateSelector' => [
41 'renderType' => 'localizationStateSelector',
42 'after' => [
43 'selectIcons',
44 ],
45 ],
46 'otherLanguageContent' => [
47 'renderType' => 'otherLanguageContent',
48 'after' => [ 'localizationStateSelector' ],
49 ],
50 'defaultLanguageDifferences' => [
51 'renderType' => 'defaultLanguageDifferences',
52 'after' => [ 'otherLanguageContent' ],
53 ],
54 ];
55
56 /**
57 * Render single element
58 *
59 * @return array As defined in initializeResultArray() of AbstractNode
60 */
61 public function render()
62 {
63 $resultArray = $this->initializeResultArray();
64
65 $table = $this->data['tableName'];
66 $field = $this->data['fieldName'];
67 $row = $this->data['databaseRow'];
68 $parameterArray = $this->data['parameterArray'];
69 $config = $parameterArray['fieldConf']['config'];
70
71 $selectItems = $parameterArray['fieldConf']['config']['items'];
72
73 // Check against inline uniqueness
74 /** @var InlineStackProcessor $inlineStackProcessor */
75 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
76 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
77 $uniqueIds = null;
78 if ($this->data['isInlineChild'] && $this->data['inlineParentUid']) {
79 // @todo: At least parts of this if is dead and/or broken: $uniqueIds is filled but never used.
80 // See InlineControlContainer where 'inlineData' 'unique' 'used' is set. What exactly is
81 // this if supposed to do and when should it kick in and what for?
82 $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
83 $inlineFormName = $inlineStackProcessor->getCurrentStructureFormPrefix();
84 if ($this->data['inlineParentConfig']['foreign_table'] === $table
85 && $this->data['inlineParentConfig']['foreign_unique'] === $field
86 ) {
87 $uniqueIds = $this->data['inlineData']['unique'][$inlineObjectName . '-' . $table]['used'];
88 $parameterArray['fieldChangeFunc']['inlineUnique'] = 'inline.updateUnique(this,'
89 . GeneralUtility::quoteJSvalue($inlineObjectName . '-' . $table) . ','
90 . GeneralUtility::quoteJSvalue($inlineFormName) . ','
91 . GeneralUtility::quoteJSvalue($row['uid']) . ');';
92 }
93 // hide uid of parent record for symmetric relations
94 if ($this->data['inlineParentConfig']['foreign_table'] === $table
95 && (
96 $this->data['inlineParentConfig']['foreign_field'] === $field
97 || $this->data['inlineParentConfig']['symmetric_field'] === $field
98 )
99 ) {
100 $uniqueIds[] = $this->data['inlineParentUid'];
101 }
102 }
103
104 // Initialization:
105 $selectId = StringUtility::getUniqueId('tceforms-select-');
106 $selectedIcon = '';
107 $size = (int)$config['size'];
108
109 // Style set on <select/>
110 $options = '';
111 $disabled = false;
112 if (!empty($config['readOnly'])) {
113 $disabled = true;
114 }
115
116 // Prepare groups
117 $selectItemCounter = 0;
118 $selectItemGroupCount = 0;
119 $selectItemGroups = [];
120 $selectedValue = '';
121 $hasIcons = false;
122
123 if (!empty($parameterArray['itemFormElValue'])) {
124 $selectedValue = (string)$parameterArray['itemFormElValue'][0];
125 }
126
127 foreach ($selectItems as $item) {
128 if ($item[1] === '--div--') {
129 // IS OPTGROUP
130 if ($selectItemCounter !== 0) {
131 $selectItemGroupCount++;
132 }
133 $selectItemGroups[$selectItemGroupCount]['header'] = [
134 'title' => $item[0],
135 ];
136 } else {
137 // IS ITEM
138 $icon = !empty($item[2]) ? FormEngineUtility::getIconHtml($item[2], $item[0], $item[0]) : '';
139 $selected = $selectedValue === (string)$item[1];
140
141 if ($selected) {
142 $selectedIcon = $icon;
143 }
144
145 $selectItemGroups[$selectItemGroupCount]['items'][] = [
146 'title' => $item[0],
147 'value' => $item[1],
148 'icon' => $icon,
149 'selected' => $selected,
150 ];
151 $selectItemCounter++;
152 }
153 }
154
155 // Fallback icon
156 // @todo: assign a special icon for non matching values?
157 if (!$selectedIcon && $selectItemGroups[0]['items'][0]['icon']) {
158 $selectedIcon = $selectItemGroups[0]['items'][0]['icon'];
159 }
160
161 // Process groups
162 foreach ($selectItemGroups as $selectItemGroup) {
163 // suppress groups without items
164 if (empty($selectItemGroup['items'])) {
165 continue;
166 }
167
168 $optionGroup = is_array($selectItemGroup['header']);
169 $options .= ($optionGroup ? '<optgroup label="' . htmlspecialchars($selectItemGroup['header']['title'], ENT_COMPAT, 'UTF-8', false) . '">' : '');
170
171 if (is_array($selectItemGroup['items'])) {
172 foreach ($selectItemGroup['items'] as $item) {
173 $options .= '<option value="' . htmlspecialchars($item['value']) . '" data-icon="' .
174 htmlspecialchars($item['icon']) . '"'
175 . ($item['selected'] ? ' selected="selected"' : '') . '>' . htmlspecialchars($item['title'], ENT_COMPAT, 'UTF-8', false) . '</option>';
176 }
177 $hasIcons = !empty($item['icon']);
178 }
179
180 $options .= ($optionGroup ? '</optgroup>' : '');
181 }
182
183 $selectAttributes = [
184 'id' => $selectId,
185 'name' => $parameterArray['itemFormElName'],
186 'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
187 'class' => 'form-control form-control-adapt',
188 ];
189 if ($size) {
190 $selectAttributes['size'] = $size;
191 }
192 if ($disabled) {
193 $selectAttributes['disabled'] = 'disabled';
194 }
195
196 $legacyWizards = $this->renderWizards();
197 $legacyFieldWizardHtml = implode(LF, $legacyWizards['fieldWizard']);
198
199 $fieldInformationResult = $this->renderFieldInformation();
200 $fieldInformationHtml = $fieldInformationResult['html'];
201 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
202
203 $fieldWizardResult = $this->renderFieldWizard();
204 $fieldWizardHtml = $legacyFieldWizardHtml . $fieldWizardResult['html'];
205 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
206
207 $html = [];
208 $html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
209 if (!$disabled) {
210 $html[] = $fieldInformationHtml;
211 }
212 $html[] = '<div class="form-control-wrap">';
213 $html[] = '<div class="form-wizards-wrap">';
214 $html[] = '<div class="form-wizards-element">';
215 if ($hasIcons) {
216 $html[] = '<div class="input-group">';
217 $html[] = '<span class="input-group-addon input-group-icon">';
218 $html[] = $selectedIcon;
219 $html[] = '</span>';
220 }
221 $html[] = '<select ' . GeneralUtility::implodeAttributes($selectAttributes, true) . '>';
222 $html[] = $options;
223 $html[] = '</select>';
224 if ($hasIcons) {
225 $html[] = '</div>';
226 }
227 $html[] = '</div>';
228 if (!$disabled) {
229 $html[] = '<div class="form-wizards-items-bottom">';
230 $html[] = $fieldWizardHtml;
231 $html[] = '</div>';
232 }
233 $html[] = '</div>';
234 $html[] = '</div>';
235 $html[] = '</div>';
236
237 $resultArray['requireJsModules'][] = ['TYPO3/CMS/Backend/FormEngine/Element/SelectSingleElement' => implode(LF, [
238 'function(SelectSingleElement) {',
239 'require([\'jquery\'], function($) {',
240 '$(function() {',
241 'SelectSingleElement.initialize(',
242 GeneralUtility::quoteJSvalue('#' . $selectId) . ',',
243 '{',
244 'onChange: function() {',
245 implode('', $parameterArray['fieldChangeFunc']),
246 '}',
247 '}',
248 ');',
249 '});',
250 '});',
251 '}',
252 ])];
253
254 $resultArray['html'] = implode(LF, $html);
255 return $resultArray;
256 }
257 }