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