[!!!][TASK] Improve flex and TCA handling in FormEngine
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / Element / AbstractFormElement.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\AbstractNode;
18 use TYPO3\CMS\Backend\Form\FormDataCompiler;
19 use TYPO3\CMS\Backend\Form\FormDataGroup\OnTheFly;
20 use TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectItems;
21 use TYPO3\CMS\Backend\Form\NodeFactory;
22 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
23 use TYPO3\CMS\Backend\Form\Wizard\SuggestWizard;
24 use TYPO3\CMS\Backend\Form\Wizard\ValueSliderWizard;
25 use TYPO3\CMS\Backend\Utility\BackendUtility;
26 use TYPO3\CMS\Core\Imaging\IconFactory;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28 use TYPO3\CMS\Core\Utility\MathUtility;
29 use TYPO3\CMS\Core\Utility\StringUtility;
30 use TYPO3\CMS\Lang\LanguageService;
31
32 /**
33 * Base class for form elements of FormEngine. Contains several helper methods used by single elements.
34 */
35 abstract class AbstractFormElement extends AbstractNode
36 {
37 /**
38 * Default width value for a couple of elements like text
39 *
40 * @var int
41 */
42 protected $defaultInputWidth = 30;
43
44 /**
45 * Minimum width value for a couple of elements like text
46 *
47 * @var int
48 */
49 protected $minimumInputWidth = 10;
50
51 /**
52 * Maximum width value for a couple of elements like text
53 *
54 * @var int
55 */
56 protected $maxInputWidth = 50;
57
58 /**
59 * @var NodeFactory
60 */
61 protected $nodeFactory;
62
63 /**
64 * Container objects give $nodeFactory down to other containers.
65 *
66 * @param NodeFactory $nodeFactory
67 * @param array $data
68 */
69 public function __construct(NodeFactory $nodeFactory, array $data)
70 {
71 parent::__construct($nodeFactory, $data);
72 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
73 // @todo: this must vanish as soon as elements are clean
74 $this->nodeFactory = $nodeFactory;
75 }
76
77 /**
78 * @return bool TRUE if wizards are disabled on a global level
79 */
80 protected function isWizardsDisabled()
81 {
82 return !empty($this->data['disabledWizards']);
83 }
84
85 /**
86 * Returns the max width in pixels for an elements like input and text
87 *
88 * @param int $size The abstract size value (1-48)
89 * @return int Maximum width in pixels
90 */
91 protected function formMaxWidth($size = 48)
92 {
93 $compensationForLargeDocuments = 1.33;
94 $compensationForFormFields = 12;
95
96 $size = round($size * $compensationForLargeDocuments);
97 return ceil($size * $compensationForFormFields);
98 }
99
100 /**
101 * @var IconFactory
102 */
103 protected $iconFactory;
104
105 /**
106 * Rendering wizards for form fields.
107 *
108 * @param array $itemKinds Array with the real item in the first value
109 * @param array $wizConf The "wizards" key from the config array for the field (from TCA)
110 * @param string $table Table name
111 * @param array $row The record array
112 * @param string $field The field name
113 * @param array $PA Additional configuration array.
114 * @param string $itemName The field name
115 * @param array $specConf Special configuration if available.
116 * @param bool $RTE Whether the RTE could have been loaded.
117 *
118 * @return string The new item value.
119 * @throws \InvalidArgumentException
120 */
121 protected function renderWizards($itemKinds, $wizConf, $table, $row, $field, $PA, $itemName, $specConf, $RTE = false)
122 {
123 // Return not changed main item directly if wizards are disabled
124 if (!is_array($wizConf) || $this->isWizardsDisabled()) {
125 return $itemKinds[0];
126 }
127
128 $languageService = $this->getLanguageService();
129
130 $fieldChangeFunc = $PA['fieldChangeFunc'];
131 $item = $itemKinds[0];
132 $md5ID = 'ID' . GeneralUtility::shortMD5($itemName);
133 $prefixOfFormElName = 'data[' . $table . '][' . $row['uid'] . '][' . $field . ']';
134 $flexFormPath = '';
135 if (GeneralUtility::isFirstPartOfStr($PA['itemFormElName'], $prefixOfFormElName)) {
136 $flexFormPath = str_replace('][', '/', substr($PA['itemFormElName'], strlen($prefixOfFormElName) + 1, -1));
137 }
138
139 // Add a suffix-value if the item is a selector box with renderType "selectSingleBox":
140 if ($PA['fieldConf']['config']['type'] === 'select' && (int)$PA['fieldConf']['config']['maxitems'] > 1 && $PA['fieldConf']['config']['renderType'] === 'selectSingleBox') {
141 $itemName .= '[]';
142 }
143
144 // Contains wizard identifiers enabled for this record type, see "special configuration" docs
145 $wizardsEnabledByType = $specConf['wizards']['parameters'];
146
147 $buttonWizards = [];
148 $otherWizards = [];
149 foreach ($wizConf as $wizardIdentifier => $wizardConfiguration) {
150 if (!isset($wizardConfiguration['module']['name']) && isset($wizardConfiguration['script'])) {
151 throw new \InvalidArgumentException('The way registering a wizard in TCA has changed in 6.2 and was removed in CMS 7. '
152 . 'Please set module[name]=module_name instead of using script=path/to/script.php in your TCA. ', 1437750231);
153 }
154
155 // If an identifier starts with "_", this is a configuration option like _POSITION and not a wizard
156 if ($wizardIdentifier[0] === '_') {
157 continue;
158 }
159
160 // Sanitize wizard type
161 $wizardConfiguration['type'] = (string)$wizardConfiguration['type'];
162
163 // Wizards can be shown based on selected "type" of record. If this is the case, the wizard configuration
164 // is set to enableByTypeConfig = 1, and the wizardIdentifier is found in $wizardsEnabledByType
165 $wizardIsEnabled = true;
166 if (
167 isset($wizardConfiguration['enableByTypeConfig'])
168 && (bool)$wizardConfiguration['enableByTypeConfig']
169 && (!is_array($wizardsEnabledByType) || !in_array($wizardIdentifier, $wizardsEnabledByType))
170 ) {
171 $wizardIsEnabled = false;
172 }
173 // Disable if wizard is for RTE fields only and the handled field is no RTE field or RTE can not be loaded
174 if (isset($wizardConfiguration['RTEonly']) && (bool)$wizardConfiguration['RTEonly'] && !$RTE) {
175 $wizardIsEnabled = false;
176 }
177 // Disable if wizard is for not-new records only and we're handling a new record
178 if (isset($wizardConfiguration['notNewRecords']) && $wizardConfiguration['notNewRecords'] && !MathUtility::canBeInterpretedAsInteger($row['uid'])) {
179 $wizardIsEnabled = false;
180 }
181 // Wizard types script, colorbox and popup must contain a module name configuration
182 if (!isset($wizardConfiguration['module']['name']) && in_array($wizardConfiguration['type'], ['script', 'colorbox', 'popup'], true)) {
183 $wizardIsEnabled = false;
184 }
185
186 if (!$wizardIsEnabled) {
187 continue;
188 }
189
190 // Title / icon:
191 $iTitle = htmlspecialchars($languageService->sL($wizardConfiguration['title']));
192 if (isset($wizardConfiguration['icon'])) {
193 $icon = FormEngineUtility::getIconHtml($wizardConfiguration['icon'], $iTitle, $iTitle);
194 } else {
195 $icon = $iTitle;
196 }
197
198 switch ($wizardConfiguration['type']) {
199 case 'userFunc':
200 $params = [];
201 $params['params'] = $wizardConfiguration['params'];
202 $params['exampleImg'] = $wizardConfiguration['exampleImg'];
203 $params['table'] = $table;
204 $params['uid'] = $row['uid'];
205 $params['pid'] = $row['pid'];
206 $params['field'] = $field;
207 $params['flexFormPath'] = $flexFormPath;
208 $params['md5ID'] = $md5ID;
209 $params['returnUrl'] = $this->data['returnUrl'];
210
211 $params['formName'] = 'editform';
212 $params['itemName'] = $itemName;
213 $params['hmac'] = GeneralUtility::hmac($params['formName'] . $params['itemName'], 'wizard_js');
214 $params['fieldChangeFunc'] = $fieldChangeFunc;
215 $params['fieldChangeFuncHash'] = GeneralUtility::hmac(serialize($fieldChangeFunc));
216
217 $params['item'] = &$item;
218 $params['icon'] = $icon;
219 $params['iTitle'] = $iTitle;
220 $params['wConf'] = $wizardConfiguration;
221 $params['row'] = $row;
222 $otherWizards[] = GeneralUtility::callUserFunction($wizardConfiguration['userFunc'], $params, $this);
223 break;
224
225 case 'script':
226 $params = [];
227 $params['params'] = $wizardConfiguration['params'];
228 $params['exampleImg'] = $wizardConfiguration['exampleImg'];
229 $params['table'] = $table;
230 $params['uid'] = $row['uid'];
231 $params['pid'] = $row['pid'];
232 $params['field'] = $field;
233 $params['flexFormPath'] = $flexFormPath;
234 $params['md5ID'] = $md5ID;
235 $params['returnUrl'] = $this->data['returnUrl'];
236
237 // Resolving script filename and setting URL.
238 $urlParameters = [];
239 if (isset($wizardConfiguration['module']['urlParameters']) && is_array($wizardConfiguration['module']['urlParameters'])) {
240 $urlParameters = $wizardConfiguration['module']['urlParameters'];
241 }
242 $wScript = BackendUtility::getModuleUrl($wizardConfiguration['module']['name'], $urlParameters, '');
243 $url = $wScript . (strstr($wScript, '?') ? '' : '?') . GeneralUtility::implodeArrayForUrl('', ['P' => $params]);
244 $buttonWizards[] =
245 '<a class="btn btn-default" href="' . htmlspecialchars($url) . '" onclick="this.blur(); return !TBE_EDITOR.isFormChanged();">'
246 . $icon .
247 '</a>';
248 break;
249
250 case 'popup':
251 $params = [];
252 $params['params'] = $wizardConfiguration['params'];
253 $params['exampleImg'] = $wizardConfiguration['exampleImg'];
254 $params['table'] = $table;
255 $params['uid'] = $row['uid'];
256 $params['pid'] = $row['pid'];
257 $params['field'] = $field;
258 $params['flexFormPath'] = $flexFormPath;
259 $params['md5ID'] = $md5ID;
260 $params['returnUrl'] = $this->data['returnUrl'];
261
262 $params['formName'] = 'editform';
263 $params['itemName'] = $itemName;
264 $params['hmac'] = GeneralUtility::hmac($params['formName'] . $params['itemName'], 'wizard_js');
265 $params['fieldChangeFunc'] = $fieldChangeFunc;
266 $params['fieldChangeFuncHash'] = GeneralUtility::hmac(serialize($fieldChangeFunc));
267
268 // Resolving script filename and setting URL.
269 $urlParameters = [];
270 if (isset($wizardConfiguration['module']['urlParameters']) && is_array($wizardConfiguration['module']['urlParameters'])) {
271 $urlParameters = $wizardConfiguration['module']['urlParameters'];
272 }
273 $wScript = BackendUtility::getModuleUrl($wizardConfiguration['module']['name'], $urlParameters, '');
274 $url = $wScript . (strstr($wScript, '?') ? '' : '?') . GeneralUtility::implodeArrayForUrl('', ['P' => $params]);
275
276 $onlyIfSelectedJS = '';
277 if (isset($wizardConfiguration['popup_onlyOpenIfSelected']) && $wizardConfiguration['popup_onlyOpenIfSelected']) {
278 $notSelectedText = $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:mess.noSelItemForEdit');
279 $onlyIfSelectedJS =
280 'if (!TBE_EDITOR.curSelected(' . GeneralUtility::quoteJSvalue($itemName) . ')){' .
281 'alert(' . GeneralUtility::quoteJSvalue($notSelectedText) . ');' .
282 'return false;' .
283 '}';
284 }
285 $aOnClick =
286 'this.blur();' .
287 $onlyIfSelectedJS .
288 'vHWin=window.open(' . GeneralUtility::quoteJSvalue($url) . '+\'&P[currentValue]=\'+TBE_EDITOR.rawurlencode(' .
289 'document.editform[' . GeneralUtility::quoteJSvalue($itemName) . '].value,300' .
290 ')' .
291 '+\'&P[currentSelectedValues]=\'+TBE_EDITOR.curSelected(' . GeneralUtility::quoteJSvalue($itemName) . '),' .
292 GeneralUtility::quoteJSvalue('popUp' . $md5ID) . ',' .
293 GeneralUtility::quoteJSvalue($wizardConfiguration['JSopenParams']) .
294 ');' .
295 'vHWin.focus();' .
296 'return false;';
297
298 $buttonWizards[] =
299 '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars($aOnClick) . '">' .
300 $icon .
301 '</a>';
302 break;
303
304 case 'slider':
305 $params = [];
306 $params['fieldConfig'] = $PA['fieldConf']['config'];
307 $params['field'] = $field;
308 $params['table'] = $table;
309 $params['flexFormPath'] = $flexFormPath;
310 $params['md5ID'] = $md5ID;
311 $params['itemName'] = $itemName;
312 $params['wConf'] = $wizardConfiguration;
313 $params['row'] = $row;
314
315 /** @var ValueSliderWizard $wizard */
316 $wizard = GeneralUtility::makeInstance(ValueSliderWizard::class);
317 $otherWizards[] = $wizard->renderWizard($params);
318 break;
319
320 case 'select':
321 // The select wizard is a select drop down added to the main element. It provides all the functionality
322 // that select items can do for us, so we process this element via data processing.
323 // @todo: This should be embedded in an own provider called in the main data group to not handle this on the fly here
324
325 // Select wizard page TS can be set in TCEFORM."table"."field".wizards."wizardName"
326 $pageTsConfig = [];
327 if (isset($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$field . '.']['wizards.'][$wizardIdentifier . '.'])
328 && is_array($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$field . '.']['wizards.'][$wizardIdentifier . '.'])
329 ) {
330 $pageTsConfig['TCEFORM.']['dummySelectWizard.'][$wizardIdentifier . '.'] = $this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$field . '.']['wizards.'][$wizardIdentifier . '.'];
331 }
332 $selectWizardDataInput = [
333 'tableName' => 'dummySelectWizard',
334 'command' => 'edit',
335 'pageTsConfig' => $pageTsConfig,
336 'processedTca' => [
337 'ctrl' => [],
338 'columns' => [
339 $wizardIdentifier => [
340 'type' => 'select',
341 'renderType' => 'selectSingle',
342 'config' => $wizardConfiguration,
343 ],
344 ],
345 ],
346 ];
347 /** @var OnTheFly $formDataGroup */
348 $formDataGroup = GeneralUtility::makeInstance(OnTheFly::class);
349 $formDataGroup->setProviderList([ TcaSelectItems::class ]);
350 /** @var FormDataCompiler $formDataCompiler */
351 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
352 $compilerResult = $formDataCompiler->compile($selectWizardDataInput);
353 $selectWizardItems = $compilerResult['processedTca']['columns'][$wizardIdentifier]['config']['items'];
354
355 $options = [];
356 $options[] = '<option>' . $iTitle . '</option>';
357 foreach ($selectWizardItems as $selectWizardItem) {
358 $options[] = '<option value="' . htmlspecialchars($selectWizardItem[1]) . '">' . htmlspecialchars($selectWizardItem[0]) . '</option>';
359 }
360 if ($wizardConfiguration['mode'] == 'append') {
361 $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0].value=\'\'+this.options[this.selectedIndex].value+document.editform[' . GeneralUtility::quoteJSvalue($itemName) . '].value';
362 } elseif ($wizardConfiguration['mode'] == 'prepend') {
363 $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0].value+=\'\'+this.options[this.selectedIndex].value';
364 } else {
365 $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0].value=this.options[this.selectedIndex].value';
366 }
367 $otherWizards[] =
368 '<select' .
369 ' id="' . StringUtility::getUniqueId('tceforms-select-') . '"' .
370 ' class="form-control tceforms-select tceforms-wizardselect"' .
371 ' onchange="' . htmlspecialchars($assignValue . ';this.blur();this.selectedIndex=0;' . implode('', $fieldChangeFunc)) . '"' .
372 '>' .
373 implode('', $options) .
374 '</select>';
375 break;
376 case 'suggest':
377 if (!empty($PA['fieldTSConfig']['suggest.']['default.']['hide'])) {
378 break;
379 }
380 $suggestWizard = GeneralUtility::makeInstance(SuggestWizard::class);
381 $otherWizards[] = $suggestWizard->renderSuggestSelector($this->data);
382 break;
383 }
384 }
385
386 // For each rendered wizard, put them together around the item.
387 if (!empty($buttonWizards) || !empty($otherWizards)) {
388 $innerContent = '';
389 if (!empty($buttonWizards)) {
390 $innerContent .= '<div class="btn-group' . ($wizConf['_VERTICAL'] ? ' btn-group-vertical' : '') . '">' . implode('', $buttonWizards) . '</div>';
391 }
392 $innerContent .= implode(' ', $otherWizards);
393
394 // Position
395 $classes = ['form-wizards-wrap'];
396 if ($wizConf['_POSITION'] === 'left') {
397 $classes[] = 'form-wizards-aside';
398 $innerContent = '<div class="form-wizards-items">' . $innerContent . '</div><div class="form-wizards-element">' . $item . '</div>';
399 } elseif ($wizConf['_POSITION'] === 'top') {
400 $classes[] = 'form-wizards-top';
401 $innerContent = '<div class="form-wizards-items">' . $innerContent . '</div><div class="form-wizards-element">' . $item . '</div>';
402 } elseif ($wizConf['_POSITION'] === 'bottom') {
403 $classes[] = 'form-wizards-bottom';
404 $innerContent = '<div class="form-wizards-element">' . $item . '</div><div class="form-wizards-items">' . $innerContent . '</div>';
405 } else {
406 $classes[] = 'form-wizards-aside';
407 $innerContent = '<div class="form-wizards-element">' . $item . '</div><div class="form-wizards-items">' . $innerContent . '</div>';
408 }
409 $item = '
410 <div class="' . implode(' ', $classes) . '">
411 ' . $innerContent . '
412 </div>';
413 }
414
415 return $item;
416 }
417
418 /**
419 * @return LanguageService
420 */
421 protected function getLanguageService()
422 {
423 return $GLOBALS['LANG'];
424 }
425 }