[BUGFIX] FormEngine: Select correct FlexForm DS for inline records
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / Container / InlineControlContainer.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form\Container;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Form\InlineStackProcessor;
18 use TYPO3\CMS\Backend\Form\NodeFactory;
19 use TYPO3\CMS\Backend\Utility\BackendUtility;
20 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21 use TYPO3\CMS\Core\Imaging\Icon;
22 use TYPO3\CMS\Core\Imaging\IconFactory;
23 use TYPO3\CMS\Core\Resource\Folder;
24 use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26 use TYPO3\CMS\Core\Utility\MathUtility;
27 use TYPO3\CMS\Lang\LanguageService;
28
29 /**
30 * Inline element entry container.
31 *
32 * This container is the entry step to rendering an inline element. It is created by SingleFieldContainer.
33 *
34 * The code creates the main structure for the single inline elements, initializes
35 * the inlineData array, that is manipulated and also returned back in its manipulated state.
36 * The "control" stuff of inline elements is rendered here, for example the "create new" button.
37 *
38 * For each existing inline relation an InlineRecordContainer is called for further processing.
39 */
40 class InlineControlContainer extends AbstractContainer
41 {
42 /**
43 * Inline data array used in JS, returned as JSON object to frontend
44 *
45 * @var array
46 */
47 protected $inlineData = array();
48
49 /**
50 * @var InlineStackProcessor
51 */
52 protected $inlineStackProcessor;
53
54 /**
55 * @var IconFactory
56 */
57 protected $iconFactory;
58
59 /**
60 * @var string[]
61 */
62 protected $requireJsModules = [];
63
64 /**
65 * Container objects give $nodeFactory down to other containers.
66 *
67 * @param NodeFactory $nodeFactory
68 * @param array $data
69 */
70 public function __construct(NodeFactory $nodeFactory, array $data)
71 {
72 parent::__construct($nodeFactory, $data);
73 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
74 }
75
76 /**
77 * Entry method
78 *
79 * @return array As defined in initializeResultArray() of AbstractNode
80 */
81 public function render()
82 {
83 $languageService = $this->getLanguageService();
84
85 $this->inlineData = $this->data['inlineData'];
86
87 /** @var InlineStackProcessor $inlineStackProcessor */
88 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
89 $this->inlineStackProcessor = $inlineStackProcessor;
90 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
91
92 $table = $this->data['tableName'];
93 $row = $this->data['databaseRow'];
94 $field = $this->data['fieldName'];
95 $parameterArray = $this->data['parameterArray'];
96
97 $resultArray = $this->initializeResultArray();
98
99 $config = $parameterArray['fieldConf']['config'];
100 $foreign_table = $config['foreign_table'];
101
102 $language = 0;
103 if (BackendUtility::isTableLocalizable($table)) {
104 $language = (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
105 }
106
107 // Add the current inline job to the structure stack
108 $newStructureItem = array(
109 'table' => $table,
110 'uid' => $row['uid'],
111 'field' => $field,
112 'config' => $config,
113 'localizationMode' => BackendUtility::getInlineLocalizationMode($table, $config),
114 );
115 // Extract FlexForm parts (if any) from element name, e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF')
116 if (!empty($parameterArray['itemFormElName'])) {
117 $flexFormParts = $this->extractFlexFormParts($parameterArray['itemFormElName']);
118 if ($flexFormParts !== null) {
119 $newStructureItem['flexform'] = $flexFormParts;
120 }
121 }
122 $inlineStackProcessor->pushStableStructureItem($newStructureItem);
123
124 // Transport the flexform DS identifier fields to the FormAjaxInlineController
125 if (!empty($newStructureItem['flexform'])
126 && isset($this->data['processedTca']['columns'][$field]['config']['ds']['meta']['dataStructurePointers'])
127 ) {
128 $config['flexDataStructurePointers'] = $this->data['processedTca']['columns'][$field]['config']['ds']['meta']['dataStructurePointers'];
129 }
130
131 // e.g. data[<table>][<uid>][<field>]
132 $nameForm = $inlineStackProcessor->getCurrentStructureFormPrefix();
133 // e.g. data-<pid>-<table1>-<uid1>-<field1>-<table2>-<uid2>-<field2>
134 $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
135
136 $config['inline']['first'] = false;
137 // @todo: This initialization shouldn't be required data provider should take care this is set?
138 if (!is_array($this->data['parameterArray']['fieldConf']['children'])) {
139 $this->data['parameterArray']['fieldConf']['children'] = array();
140 }
141 $firstChild = reset($this->data['parameterArray']['fieldConf']['children']);
142 if (isset($firstChild['databaseRow']['uid'])) {
143 $config['inline']['first'] = $firstChild['databaseRow']['uid'];
144 }
145 $config['inline']['last'] = false;
146 $lastChild = end($this->data['parameterArray']['fieldConf']['children']);
147 if (isset($lastChild['databaseRow']['uid'])) {
148 $config['inline']['last'] = $lastChild['databaseRow']['uid'];
149 }
150
151 $top = $inlineStackProcessor->getStructureLevel(0);
152
153 $this->inlineData['config'][$nameObject] = array(
154 'table' => $foreign_table,
155 'md5' => md5($nameObject)
156 );
157 $this->inlineData['config'][$nameObject . '-' . $foreign_table] = array(
158 'min' => $config['minitems'],
159 'max' => $config['maxitems'],
160 'sortable' => $config['appearance']['useSortable'],
161 'top' => array(
162 'table' => $top['table'],
163 'uid' => $top['uid']
164 ),
165 'context' => array(
166 'config' => $config,
167 'hmac' => GeneralUtility::hmac(serialize($config)),
168 ),
169 );
170 $this->inlineData['nested'][$nameObject] = $this->data['tabAndInlineStack'];
171
172 $uniqueMax = 0;
173 $uniqueIds = [];
174
175 if ($config['foreign_unique']) {
176 // Add inlineData['unique'] with JS unique configuration
177 $type = $config['selectorOrUniqueConfiguration']['config']['type'] === 'select' ? 'select' : 'groupdb';
178 foreach ($parameterArray['fieldConf']['children'] as $child) {
179 // Determine used unique ids, skip not localized records
180 if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
181 $value = $child['databaseRow'][$config['foreign_unique']];
182 // We're assuming there is only one connected value here for both select and group
183 if ($type === 'select') {
184 // A resolved select field is an array - take first value
185 $value = $value['0'];
186 } else {
187 // A group field is still a list with pipe separated uid|tableName
188 $valueParts = GeneralUtility::trimExplode('|', $value);
189 $itemParts = explode('_', $valueParts[0]);
190 $value = array(
191 'uid' => array_pop($itemParts),
192 'table' => implode('_', $itemParts)
193 );
194 }
195 // @todo: This is weird, $value has different structure for group and select fields?
196 $uniqueIds[$child['databaseRow']['uid']] = $value;
197 }
198 }
199 $possibleRecords = $config['selectorOrUniquePossibleRecords'];
200 $possibleRecordsUidToTitle = [];
201 foreach ($possibleRecords as $possibleRecord) {
202 $possibleRecordsUidToTitle[$possibleRecord[1]] = $possibleRecord[0];
203 }
204 $uniqueMax = $config['appearance']['useCombination'] || empty($possibleRecords) ? -1 : count($possibleRecords);
205 $this->inlineData['unique'][$nameObject . '-' . $foreign_table] = array(
206 'max' => $uniqueMax,
207 'used' => $uniqueIds,
208 'type' => $type,
209 'table' => $foreign_table,
210 'elTable' => $config['selectorOrUniqueConfiguration']['foreignTable'],
211 'field' => $config['foreign_unique'],
212 'selector' => $config['selectorOrUniqueConfiguration']['isSelector'] ? $type : false,
213 'possible' => $possibleRecordsUidToTitle,
214 );
215 }
216
217 $resultArray['inlineData'] = $this->inlineData;
218
219 // @todo: It might be a good idea to have something like "isLocalizedRecord" or similar set by a data provider
220 $isLocalizedParent = $language > 0
221 && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']][0] > 0
222 && MathUtility::canBeInterpretedAsInteger($row['uid']);
223 $numberOfFullLocalizedChildren = 0;
224 $numberOfNotYetLocalizedChildren = 0;
225 foreach ($this->data['parameterArray']['fieldConf']['children'] as $child) {
226 if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
227 $numberOfFullLocalizedChildren ++;
228 }
229 if ($isLocalizedParent && $child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
230 $numberOfNotYetLocalizedChildren ++;
231 }
232 }
233
234 // Render the localization links if needed
235 $localizationLinks = '';
236 if ($numberOfNotYetLocalizedChildren) {
237 // Add the "Localize all records" link before all child records:
238 if (isset($config['appearance']['showAllLocalizationLink']) && $config['appearance']['showAllLocalizationLink']) {
239 $localizationLinks = ' ' . $this->getLevelInteractionLink('localize', $nameObject . '-' . $foreign_table, $config);
240 }
241 // Add the "Synchronize with default language" link before all child records:
242 if (isset($config['appearance']['showSynchronizationLink']) && $config['appearance']['showSynchronizationLink']) {
243 $localizationLinks .= ' ' . $this->getLevelInteractionLink('synchronize', $nameObject . '-' . $foreign_table, $config);
244 }
245 }
246
247 // Define how to show the "Create new record" link - if there are more than maxitems, hide it
248 if ($numberOfFullLocalizedChildren >= $config['maxitems'] || $uniqueMax > 0 && $numberOfFullLocalizedChildren >= $uniqueMax) {
249 $config['inline']['inlineNewButtonStyle'] = 'display: none;';
250 $config['inline']['inlineNewRelationButtonStyle'] = 'display: none;';
251 }
252
253 // Render the level links (create new record):
254 $levelLinks = '';
255 if (!empty($config['appearance']['enabledControls']['new'])) {
256 $levelLinks = $this->getLevelInteractionLink('newRecord', $nameObject . '-' . $foreign_table, $config);
257 }
258 // Wrap all inline fields of a record with a <div> (like a container)
259 $html = '<div class="form-group" id="' . $nameObject . '">';
260 // Add the level links before all child records:
261 if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'top') {
262 $html .= '<div class="form-group t3js-formengine-validation-marker">' . $levelLinks . $localizationLinks . '</div>';
263 }
264
265 // If it's required to select from possible child records (reusable children), add a selector box
266 if ($config['foreign_selector'] && $config['appearance']['showPossibleRecordsSelector'] !== false) {
267 if ($config['selectorOrUniqueConfiguration']['config']['type'] === 'select') {
268 $selectorBox = $this->renderPossibleRecordsSelectorTypeSelect($config, $uniqueIds);
269 } else {
270 $selectorBox = $this->renderPossibleRecordsSelectorTypeGroupDB($config);
271 }
272 $html .= $selectorBox . $localizationLinks;
273 }
274
275 $title = $languageService->sL(trim($parameterArray['fieldConf']['label']));
276 $html .= '<div class="panel-group panel-hover" data-title="' . htmlspecialchars($title) . '" id="' . $nameObject . '_records">';
277
278 $sortableRecordUids = [];
279 foreach ($this->data['parameterArray']['fieldConf']['children'] as $options) {
280 $options['inlineParentUid'] = $row['uid'];
281 // @todo: this can be removed if this container no longer sets additional info to $config
282 $options['inlineParentConfig'] = $config;
283 $options['inlineData'] = $this->inlineData;
284 $options['inlineStructure'] = $inlineStackProcessor->getStructure();
285 $options['inlineExpandCollapseStateArray'] = $this->data['inlineExpandCollapseStateArray'];
286 $options['renderType'] = 'inlineRecordContainer';
287 $childResult = $this->nodeFactory->create($options)->render();
288 $html .= $childResult['html'];
289 $childArray['html'] = '';
290 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult);
291 if (!$options['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
292 // Don't add record to list of "valid" uids if it is only the default
293 // language record of a not yet localized child
294 $sortableRecordUids[] = $options['databaseRow']['uid'];
295 }
296 }
297
298 $html .= '</div>';
299
300 // Add the level links after all child records:
301 if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'bottom') {
302 $html .= $levelLinks . $localizationLinks;
303 }
304 if (is_array($config['customControls'])) {
305 $html .= '<div id="' . $nameObject . '_customControls">';
306 foreach ($config['customControls'] as $customControlConfig) {
307 $parameters = array(
308 'table' => $table,
309 'field' => $field,
310 'row' => $row,
311 'nameObject' => $nameObject,
312 'nameForm' => $nameForm,
313 'config' => $config
314 );
315 $html .= GeneralUtility::callUserFunction($customControlConfig, $parameters, $this);
316 }
317 $html .= '</div>';
318 }
319 // Add Drag&Drop functions for sorting to FormEngine::$additionalJS_post
320 if (count($sortableRecordUids) > 1 && $config['appearance']['useSortable']) {
321 $resultArray['additionalJavaScriptPost'][] = 'inline.createDragAndDropSorting("' . $nameObject . '_records' . '");';
322 }
323 $resultArray['requireJsModules'] = array_merge($resultArray['requireJsModules'], $this->requireJsModules);
324
325 // Publish the uids of the child records in the given order to the browser
326 $html .= '<input type="hidden" name="' . $nameForm . '" value="' . implode(',', $sortableRecordUids) . '" '
327 . $this->getValidationDataAsDataAttribute(array('type' => 'inline', 'minitems' => $config['minitems'], 'maxitems' => $config['maxitems']))
328 . ' class="inlineRecord" />';
329 // Close the wrap for all inline fields (container)
330 $html .= '</div>';
331
332 $resultArray['html'] = $html;
333 return $resultArray;
334 }
335
336 /**
337 * Creates the HTML code of a general link to be used on a level of inline children.
338 * The possible keys for the parameter $type are 'newRecord', 'localize' and 'synchronize'.
339 *
340 * @param string $type The link type, values are 'newRecord', 'localize' and 'synchronize'.
341 * @param string $objectPrefix The "path" to the child record to create (e.g. 'data-parentPageId-partenTable-parentUid-parentField-childTable]')
342 * @param array $conf TCA configuration of the parent(!) field
343 * @return string The HTML code of the new link, wrapped in a div
344 */
345 protected function getLevelInteractionLink($type, $objectPrefix, $conf = array())
346 {
347 $languageService = $this->getLanguageService();
348 $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
349 $attributes = array();
350 switch ($type) {
351 case 'newRecord':
352 $title = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.createnew', true);
353 $icon = 'actions-document-new';
354 $className = 'typo3-newRecordLink';
355 $attributes['class'] = 'btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'];
356 $attributes['onclick'] = 'return inline.createNewRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ')';
357 if (!empty($conf['inline']['inlineNewButtonStyle'])) {
358 $attributes['style'] = $conf['inline']['inlineNewButtonStyle'];
359 }
360 if (!empty($conf['appearance']['newRecordLinkAddTitle'])) {
361 $title = sprintf(
362 $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.createnew.link', true),
363 $languageService->sL($GLOBALS['TCA'][$conf['foreign_table']]['ctrl']['title'], true)
364 );
365 } elseif (isset($conf['appearance']['newRecordLinkTitle']) && $conf['appearance']['newRecordLinkTitle'] !== '') {
366 $title = $languageService->sL($conf['appearance']['newRecordLinkTitle'], true);
367 }
368 break;
369 case 'localize':
370 $title = $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localizeAllRecords', true);
371 $icon = 'actions-document-localize';
372 $className = 'typo3-localizationLink';
373 $attributes['class'] = 'btn btn-default';
374 $attributes['onclick'] = 'return inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($objectPrefix) . ', \'localize\')';
375 break;
376 case 'synchronize':
377 $title = $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:synchronizeWithOriginalLanguage', true);
378 $icon = 'actions-document-synchronize';
379 $className = 'typo3-synchronizationLink';
380 $attributes['class'] = 'btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'];
381 $attributes['onclick'] = 'return inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($objectPrefix) . ', \'synchronize\')';
382 break;
383 default:
384 $title = '';
385 $icon = '';
386 $className = '';
387 }
388 // Create the link:
389 $icon = $icon ? $this->iconFactory->getIcon($icon, Icon::SIZE_SMALL)->render() : '';
390 $link = $this->wrapWithAnchor($icon . $title, '#', $attributes);
391 return '<div' . ($className ? ' class="' . $className . '"' : '') . 'title="' . $title . '">' . $link . '</div>';
392 }
393
394 /**
395 * Wraps a text with an anchor and returns the HTML representation.
396 *
397 * @param string $text The text to be wrapped by an anchor
398 * @param string $link The link to be used in the anchor
399 * @param array $attributes Array of attributes to be used in the anchor
400 * @return string The wrapped text as HTML representation
401 */
402 protected function wrapWithAnchor($text, $link, $attributes = array())
403 {
404 $link = trim($link);
405 $result = '<a href="' . ($link ?: '#') . '"';
406 foreach ($attributes as $key => $value) {
407 $result .= ' ' . $key . '="' . htmlspecialchars(trim($value)) . '"';
408 }
409 $result .= '>' . $text . '</a>';
410 return $result;
411 }
412
413 /**
414 * Generate a link that opens an element browser in a new window.
415 * For group/db there is no way to use a "selector" like a <select>|</select>-box.
416 *
417 * @param array $inlineConfiguration TCA inline configuration of the parent(!) field
418 * @return string A HTML link that opens an element browser in a new window
419 */
420 protected function renderPossibleRecordsSelectorTypeGroupDB(array $inlineConfiguration)
421 {
422 $backendUser = $this->getBackendUserAuthentication();
423 $languageService = $this->getLanguageService();
424
425 $groupFieldConfiguration = $inlineConfiguration['selectorOrUniqueConfiguration']['config'];
426
427 $foreign_table = $inlineConfiguration['foreign_table'];
428 $allowed = $groupFieldConfiguration['allowed'];
429 $objectPrefix = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']) . '-' . $foreign_table;
430 $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
431 $mode = 'db';
432 $showUpload = false;
433 if (!empty($inlineConfiguration['appearance']['createNewRelationLinkTitle'])) {
434 $createNewRelationText = $languageService->sL($inlineConfiguration['appearance']['createNewRelationLinkTitle'], true);
435 } else {
436 $createNewRelationText = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.createNewRelation', true);
437 }
438 if (is_array($groupFieldConfiguration['appearance'])) {
439 if (isset($groupFieldConfiguration['appearance']['elementBrowserType'])) {
440 $mode = $groupFieldConfiguration['appearance']['elementBrowserType'];
441 }
442 if ($mode === 'file') {
443 $showUpload = true;
444 }
445 if (isset($inlineConfiguration['appearance']['fileUploadAllowed'])) {
446 $showUpload = (bool)$inlineConfiguration['appearance']['fileUploadAllowed'];
447 }
448 if (isset($groupFieldConfiguration['appearance']['elementBrowserAllowed'])) {
449 $allowed = $groupFieldConfiguration['appearance']['elementBrowserAllowed'];
450 }
451 }
452 $browserParams = '|||' . $allowed . '|' . $objectPrefix . '|inline.checkUniqueElement||inline.importElement';
453 $onClick = 'setFormValueOpenBrowser(' . GeneralUtility::quoteJSvalue($mode) . ', ' . GeneralUtility::quoteJSvalue($browserParams) . '); return false;';
454
455 $buttonStyle = '';
456 if (isset($inlineConfiguration['inline']['inlineNewRelationButtonStyle'])) {
457 $buttonStyle = ' style="' . $inlineConfiguration['inline']['inlineNewRelationButtonStyle'] . '"';
458 }
459
460 $item = '
461 <a href="#" class="btn btn-default inlineNewRelationButton ' . $this->inlineData['config'][$nameObject]['md5'] . '"
462 ' . $buttonStyle . ' onclick="' . htmlspecialchars($onClick) . '" title="' . $createNewRelationText . '">
463 ' . $this->iconFactory->getIcon('actions-insert-record', Icon::SIZE_SMALL)->render() . '
464 ' . $createNewRelationText . '
465 </a>';
466 $isDirectFileUploadEnabled = (bool)$this->getBackendUserAuthentication()->uc['edit_docModuleUpload'];
467 $allowedArray = GeneralUtility::trimExplode(',', $allowed, true);
468 $onlineMediaAllowed = OnlineMediaHelperRegistry::getInstance()->getSupportedFileExtensions();
469 if (!empty($allowedArray)) {
470 $onlineMediaAllowed = array_intersect($allowedArray, $onlineMediaAllowed);
471 }
472 if ($showUpload && $isDirectFileUploadEnabled) {
473 $folder = $backendUser->getDefaultUploadFolder(
474 $this->data['parentPageRow']['uid'],
475 $this->data['tableName'],
476 $this->data['fieldName']
477 );
478 if (
479 $folder instanceof Folder
480 && $folder->checkActionPermission('add')
481 ) {
482 $maxFileSize = GeneralUtility::getMaxUploadFileSize() * 1024;
483 $item .= ' <a href="#" class="btn btn-default t3js-drag-uploader inlineNewFileUploadButton ' . $this->inlineData['config'][$nameObject]['md5'] . '"
484 ' . $buttonStyle . '
485 data-dropzone-target="#' . htmlspecialchars($this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid'])) . '"
486 data-insert-dropzone-before="1"
487 data-file-irre-object="' . htmlspecialchars($objectPrefix) . '"
488 data-file-allowed="' . htmlspecialchars($allowed) . '"
489 data-target-folder="' . htmlspecialchars($folder->getCombinedIdentifier()) . '"
490 data-max-file-size="' . htmlspecialchars($maxFileSize) . '"
491 >';
492 $item .= $this->iconFactory->getIcon('actions-upload', Icon::SIZE_SMALL)->render() . ' ';
493 $item .= $languageService->sL('LLL:EXT:lang/locallang_core.xlf:file_upload.select-and-submit', true);
494 $item .= '</a>';
495
496 $this->requireJsModules[] = ['TYPO3/CMS/Backend/DragUploader' => 'function(dragUploader){dragUploader.initialize()}'];
497 if (!empty($onlineMediaAllowed)) {
498 $this->requireJsModules[] = 'TYPO3/CMS/Backend/OnlineMedia';
499 $buttonText = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:online_media.new_media.button', true);
500 $placeholder = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:online_media.new_media.placeholder', true);
501 $buttonSubmit = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:online_media.new_media.submit', true);
502 $item .= '
503 <span class="btn btn-default t3js-online-media-add-btn ' . $this->inlineData['config'][$nameObject]['md5'] . '"
504 data-file-irre-object="' . htmlspecialchars($objectPrefix) . '"
505 data-online-media-allowed="' . htmlspecialchars(implode(',', $onlineMediaAllowed)) . '"
506 data-target-folder="' . htmlspecialchars($folder->getCombinedIdentifier()) . '"
507 title="' . $buttonText . '"
508 data-btn-submit="' . $buttonSubmit . '"
509 data-placeholder="' . $placeholder . '"
510 >
511 ' . $this->iconFactory->getIcon('actions-online-media-add', Icon::SIZE_SMALL)->render() . '
512 ' . $buttonText . '</span>';
513 }
514 }
515 }
516
517 $item = '<div class="form-control-wrap">' . $item . '</div>';
518 $allowedList = '';
519 $allowedLabel = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.allowedFileExtensions', true);
520 foreach ($allowedArray as $allowedItem) {
521 $allowedList .= '<span class="label label-success">' . strtoupper($allowedItem) . '</span> ';
522 }
523 if (!empty($allowedList)) {
524 $item .= '<div class="help-block">' . $allowedLabel . '<br>' . $allowedList . '</div>';
525 }
526 $item = '<div class="form-group t3js-formengine-validation-marker">' . $item . '</div>';
527 return $item;
528 }
529
530 /**
531 * Get a selector as used for the select type, to select from all available
532 * records and to create a relation to the embedding record (e.g. like MM).
533 *
534 * @param array $config TCA inline configuration of the parent(!) field
535 * @param array $uniqueIds The uids that have already been used and should be unique
536 * @return string A HTML <select> box with all possible records
537 */
538 protected function renderPossibleRecordsSelectorTypeSelect(array $config, array $uniqueIds)
539 {
540 $possibleRecords = $config['selectorOrUniquePossibleRecords'];
541 $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
542 // Create option tags:
543 $opt = [];
544 foreach ($possibleRecords as $p) {
545 if (!in_array($p[1], $uniqueIds)) {
546 $opt[] = '<option value="' . htmlspecialchars($p[1]) . '">' . htmlspecialchars($p[0]) . '</option>';
547 }
548 }
549 // Put together the selector box:
550 $size = (int)$config['size'];
551 $size = $config['autoSizeMax'] ? MathUtility::forceIntegerInRange(count($possibleRecords) + 1, MathUtility::forceIntegerInRange($size, 1), $config['autoSizeMax']) : $size;
552 $onChange = 'return inline.importNewRecord(' . GeneralUtility::quoteJSvalue($nameObject . '-' . $config['foreign_table']) . ')';
553 $item = '
554 <select id="' . $nameObject . '-' . $config['foreign_table'] . '_selector" class="form-control"' . ($size ? ' size="' . $size . '"' : '')
555 . ' onchange="' . htmlspecialchars($onChange) . '"' . ($config['foreign_unique'] ? ' isunique="isunique"' : '') . '>
556 ' . implode('', $opt) . '
557 </select>';
558
559 if ($size <= 1) {
560 // Add a "Create new relation" link for adding new relations
561 // This is necessary, if the size of the selector is "1" or if
562 // there is only one record item in the select-box, that is selected by default
563 // The selector-box creates a new relation on using an onChange event (see some line above)
564 if (!empty($config['appearance']['createNewRelationLinkTitle'])) {
565 $createNewRelationText = $this->getLanguageService()->sL($config['appearance']['createNewRelationLinkTitle'], true);
566 } else {
567 $createNewRelationText = $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:cm.createNewRelation', true);
568 }
569 $item .= '
570 <span class="input-group-btn">
571 <a href="#" class="btn btn-default" onclick="' . htmlspecialchars($onChange) . '" title="' . $createNewRelationText . '">
572 ' . $this->iconFactory->getIcon('actions-document-new', Icon::SIZE_SMALL)->render() . $createNewRelationText . '
573 </a>
574 </span>';
575 } else {
576 $item .= '
577 <span class="input-group-btn btn"></span>';
578 }
579
580 // Wrap the selector and add a spacer to the bottom
581 $item = '<div class="input-group form-group t3js-formengine-validation-marker ' . $this->inlineData['config'][$nameObject]['md5'] . '">' . $item . '</div>';
582 return $item;
583 }
584
585 /**
586 * Extracts FlexForm parts of a form element name like
587 * data[table][uid][field][sDEF][lDEF][FlexForm][vDEF]
588 * Helper method used in inline
589 *
590 * @param string $formElementName The form element name
591 * @return array|NULL
592 */
593 protected function extractFlexFormParts($formElementName)
594 {
595 $flexFormParts = null;
596 $matches = array();
597 if (preg_match('#^data(?:\[[^]]+\]){3}(\[data\](?:\[[^]]+\]){4,})$#', $formElementName, $matches)) {
598 $flexFormParts = GeneralUtility::trimExplode(
599 '][',
600 trim($matches[1], '[]')
601 );
602 }
603 return $flexFormParts;
604 }
605
606 /**
607 * @return BackendUserAuthentication
608 */
609 protected function getBackendUserAuthentication()
610 {
611 return $GLOBALS['BE_USER'];
612 }
613
614 /**
615 * @return LanguageService
616 */
617 protected function getLanguageService()
618 {
619 return $GLOBALS['LANG'];
620 }
621 }