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