[TASK] FormEngine inline refactoring
[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 * 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 parent::__construct($nodeFactory, $data);
72 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
73 }
74
75 /**
76 * Entry method
77 *
78 * @return array As defined in initializeResultArray() of AbstractNode
79 */
80 public function render() {
81 $languageService = $this->getLanguageService();
82
83 $this->inlineData = $this->data['inlineData'];
84
85 /** @var InlineStackProcessor $inlineStackProcessor */
86 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
87 $this->inlineStackProcessor = $inlineStackProcessor;
88 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
89
90 $table = $this->data['tableName'];
91 $row = $this->data['databaseRow'];
92 $field = $this->data['fieldName'];
93 $parameterArray = $this->data['parameterArray'];
94
95 $resultArray = $this->initializeResultArray();
96
97 $config = $parameterArray['fieldConf']['config'];
98 $foreign_table = $config['foreign_table'];
99
100 $language = 0;
101 if (BackendUtility::isTableLocalizable($table)) {
102 $language = (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
103 }
104
105 // Add the current inline job to the structure stack
106 $newStructureItem = array(
107 'table' => $table,
108 'uid' => $row['uid'],
109 'field' => $field,
110 'config' => $config,
111 'localizationMode' => BackendUtility::getInlineLocalizationMode($table, $config),
112 );
113 // Extract FlexForm parts (if any) from element name, e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF')
114 if (!empty($parameterArray['itemFormElName'])) {
115 $flexFormParts = $this->extractFlexFormParts($parameterArray['itemFormElName']);
116 if ($flexFormParts !== NULL) {
117 $newStructureItem['flexform'] = $flexFormParts;
118 }
119 }
120 $inlineStackProcessor->pushStableStructureItem($newStructureItem);
121
122 // e.g. data[<table>][<uid>][<field>]
123 $nameForm = $inlineStackProcessor->getCurrentStructureFormPrefix();
124 // e.g. data-<pid>-<table1>-<uid1>-<field1>-<table2>-<uid2>-<field2>
125 $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
126
127 $config['inline']['first'] = FALSE;
128 // @todo: This initialization shouldn't be required data provider should take care this is set?
129 if (!is_array($this->data['parameterArray']['fieldConf']['children'])) {
130 $this->data['parameterArray']['fieldConf']['children'] = array();
131 }
132 $firstChild = reset($this->data['parameterArray']['fieldConf']['children']);
133 if (isset($firstChild['databaseRow']['uid'])) {
134 $config['inline']['first'] = $firstChild['databaseRow']['uid'];
135 }
136 $config['inline']['last'] = FALSE;
137 $lastChild = end($this->data['parameterArray']['fieldConf']['children']);
138 if (isset($lastChild['databaseRow']['uid'])) {
139 $config['inline']['last'] = $lastChild['databaseRow']['uid'];
140 }
141
142 $top = $inlineStackProcessor->getStructureLevel(0);
143
144 $this->inlineData['config'][$nameObject] = array(
145 'table' => $foreign_table,
146 'md5' => md5($nameObject)
147 );
148 $this->inlineData['config'][$nameObject . '-' . $foreign_table] = array(
149 'min' => $config['minitems'],
150 'max' => $config['maxitems'],
151 'sortable' => $config['appearance']['useSortable'],
152 'top' => array(
153 'table' => $top['table'],
154 'uid' => $top['uid']
155 ),
156 'context' => array(
157 'config' => $config,
158 'hmac' => GeneralUtility::hmac(serialize($config)),
159 ),
160 );
161 $this->inlineData['nested'][$nameObject] = $this->data['tabAndInlineStack'];
162
163 // If relations are required to be unique, get the uids that have already been used on the foreign side of the relation
164 $uniqueMax = 0;
165 $possibleRecords = [];
166 $uniqueIds = [];
167 if ($config['foreign_unique']) {
168 // If uniqueness *and* selector are set, they should point to the same field - so, get the configuration of one:
169 $selConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($config, $config['foreign_unique']);
170 // Get the used unique ids:
171 $uniqueIds = $this->getUniqueIds($this->data['parameterArray']['fieldConf']['children'], $config, $selConfig['type'] == 'groupdb');
172 $possibleRecords = $this->getPossibleRecords($table, $field, $row, $config, 'foreign_unique');
173 $uniqueMax = $config['appearance']['useCombination'] || $possibleRecords === FALSE ? -1 : count($possibleRecords);
174 $this->inlineData['unique'][$nameObject . '-' . $foreign_table] = array(
175 'max' => $uniqueMax,
176 'used' => $uniqueIds,
177 'type' => $selConfig['type'],
178 'table' => $config['foreign_table'],
179 'elTable' => $selConfig['table'],
180 // element/record table (one step down in hierarchy)
181 'field' => $config['foreign_unique'],
182 'selector' => $selConfig['selector'],
183 'possible' => $this->getPossibleRecordsFlat($possibleRecords)
184 );
185 }
186
187 $resultArray['inlineData'] = $this->inlineData;
188
189 // Render the localization links
190 $localizationLinks = '';
191 if ($language > 0 && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0 && MathUtility::canBeInterpretedAsInteger($row['uid'])) {
192 // Add the "Localize all records" link before all child records:
193 if (isset($config['appearance']['showAllLocalizationLink']) && $config['appearance']['showAllLocalizationLink']) {
194 $localizationLinks .= ' ' . $this->getLevelInteractionLink('localize', $nameObject . '-' . $foreign_table, $config);
195 }
196 // Add the "Synchronize with default language" link before all child records:
197 if (isset($config['appearance']['showSynchronizationLink']) && $config['appearance']['showSynchronizationLink']) {
198 $localizationLinks .= ' ' . $this->getLevelInteractionLink('synchronize', $nameObject . '-' . $foreign_table, $config);
199 }
200 }
201
202 $numberOfFullChildren = 0;
203 foreach ($this->data['parameterArray']['fieldConf']['children'] as $child) {
204 if (!$child['inlineIsDefaultLanguage']) {
205 $numberOfFullChildren ++;
206 }
207 }
208 // Define how to show the "Create new record" link - if there are more than maxitems, hide it
209 if ($numberOfFullChildren >= $config['maxitems'] || $uniqueMax > 0 && $numberOfFullChildren >= $uniqueMax) {
210 $config['inline']['inlineNewButtonStyle'] = 'display: none;';
211 $config['inline']['inlineNewRelationButtonStyle'] = 'display: none;';
212 }
213
214 // Render the level links (create new record):
215 $levelLinks = $this->getLevelInteractionLink('newRecord', $nameObject . '-' . $foreign_table, $config);
216
217 // Wrap all inline fields of a record with a <div> (like a container)
218 $html = '<div class="form-group" id="' . $nameObject . '">';
219 // Add the level links before all child records:
220 if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'top') {
221 $html .= '<div class="form-group t3js-formengine-validation-marker">' . $levelLinks . $localizationLinks . '</div>';
222 }
223
224 // If it's required to select from possible child records (reusable children), add a selector box
225 if ($config['foreign_selector'] && $config['appearance']['showPossibleRecordsSelector'] !== FALSE) {
226 // If not already set by the foreign_unique, set the possibleRecords here and the uniqueIds to an empty array
227 if (!$config['foreign_unique']) {
228 $possibleRecords = $this->getPossibleRecords($table, $field, $row, $config);
229 $uniqueIds = array();
230 }
231 $selectorBox = $this->renderPossibleRecordsSelector($possibleRecords, $config, $uniqueIds);
232 $html .= $selectorBox . $localizationLinks;
233 }
234
235 $title = $languageService->sL($parameterArray['fieldConf']['label']);
236 $html .= '<div class="panel-group panel-hover" data-title="' . htmlspecialchars($title) . '" id="' . $nameObject . '_records">';
237
238 $sortableRecordUids = [];
239 foreach ($this->data['parameterArray']['fieldConf']['children'] as $options) {
240 $options['inlineParentUid'] = $row['uid'];
241 $options['inlineParentConfig'] = $config;
242 $options['inlineData'] = $this->inlineData;
243 $options['inlineStructure'] = $inlineStackProcessor->getStructure();
244 $options['inlineExpandCollapseStateArray'] = $this->data['inlineExpandCollapseStateArray'];
245 $options['renderType'] = 'inlineRecordContainer';
246 $childResult = $this->nodeFactory->create($options)->render();
247 $html .= $childResult['html'];
248 $childArray['html'] = '';
249 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult);
250 if (!$options['inlineIsDefaultLanguage']) {
251 // Don't add record to list of "valid" uids if it is only the default
252 // language record of a not yet localized child
253 $sortableRecordUids[] = $options['databaseRow']['uid'];
254 }
255 }
256
257 $html .= '</div>';
258
259 // Add the level links after all child records:
260 if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'bottom') {
261 $html .= $levelLinks . $localizationLinks;
262 }
263 if (is_array($config['customControls'])) {
264 $html .= '<div id="' . $nameObject . '_customControls">';
265 foreach ($config['customControls'] as $customControlConfig) {
266 $parameters = array(
267 'table' => $table,
268 'field' => $field,
269 'row' => $row,
270 'nameObject' => $nameObject,
271 'nameForm' => $nameForm,
272 'config' => $config
273 );
274 $html .= GeneralUtility::callUserFunction($customControlConfig, $parameters, $this);
275 }
276 $html .= '</div>';
277 }
278 // Add Drag&Drop functions for sorting to FormEngine::$additionalJS_post
279 if (count($sortableRecordUids) > 1 && $config['appearance']['useSortable']) {
280 $resultArray['additionalJavaScriptPost'][] = 'inline.createDragAndDropSorting("' . $nameObject . '_records' . '");';
281 }
282 // Publish the uids of the child records in the given order to the browser
283 $html .= '<input type="hidden" name="' . $nameForm . '" value="' . implode(',', $sortableRecordUids) . '" '
284 . $this->getValidationDataAsDataAttribute(array('type' => 'inline', 'minitems' => $config['minitems'], 'maxitems' => $config['maxitems']))
285 . ' class="inlineRecord" />';
286 // Close the wrap for all inline fields (container)
287 $html .= '</div>';
288
289 $resultArray['html'] = $html;
290 return $resultArray;
291 }
292
293 /**
294 * Gets the uids of a select/selector that should be unique and have already been used.
295 *
296 * @param array $children All inline records on this level
297 * @param array $conf The TCA field configuration of the inline field to be rendered
298 * @param bool $splitValue For usage with group/db, values come like "tx_table_123|Title%20abc", but we need "tx_table" and "123
299 * @return array The uids, that have been used already and should be used unique
300 */
301 protected function getUniqueIds($children, $conf = array(), $splitValue = FALSE) {
302 $uniqueIds = array();
303 if (isset($conf['foreign_unique']) && $conf['foreign_unique'] && !empty($children)) {
304 foreach ($children as $child) {
305 // Skip virtual records (e.g. shown in localization mode):
306 if (!$child['inlineIsDefaultLanguage']) {
307 $value = $child[$conf['foreign_unique']];
308 if (is_array($value)) {
309 $value = $value['0'];
310 }
311 // Split the value and extract the table and uid:
312 if ($splitValue) {
313 $valueParts = GeneralUtility::trimExplode('|', $value);
314 $itemParts = explode('_', $valueParts[0]);
315 $value = array(
316 'uid' => array_pop($itemParts),
317 'table' => implode('_', $itemParts)
318 );
319 }
320 $uniqueIds[$child['uid']] = $value;
321 }
322 }
323 }
324 return $uniqueIds;
325 }
326
327 /**
328 * Get possible records.
329 * Copied from FormEngine and modified.
330 *
331 * @param string $table The table name of the record
332 * @param string $field The field name which this element is supposed to edit
333 * @param array $row The record data array where the value(s) for the field can be found
334 * @param array $conf An array with additional configuration options.
335 * @param string $checkForConfField For which field in the foreign_table the possible records should be fetched
336 * @return mixed Array of possible record items; FALSE if type is "group/db", then everything could be "possible
337 */
338 protected function getPossibleRecords($table, $field, $row, $conf, $checkForConfField = 'foreign_selector') {
339 // Field configuration from TCA:
340 $foreign_check = $conf[$checkForConfField];
341 $foreignConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($conf, $foreign_check);
342 $PA = $foreignConfig['PA'];
343 if ($foreignConfig['type'] == 'select') {
344 $pageTsConfig['TCEFORM.']['dummyTable.']['dummyField.'] = $PA['fieldTSConfig'];
345 $selectDataInput = [
346 'tableName' => 'dummyTable',
347 'command' => 'edit',
348 'pageTsConfigMerged' => $pageTsConfig,
349 'vanillaTableTca' => [
350 'ctrl' => [],
351 'columns' => [
352 'dummyField' => $PA['fieldConf'],
353 ],
354 ],
355 'processedTca' => [
356 'ctrl' => [],
357 'columns' => [
358 'dummyField' => $PA['fieldConf'],
359 ],
360 ],
361 ];
362
363 /** @var OnTheFly $formDataGroup */
364 $formDataGroup = GeneralUtility::makeInstance(OnTheFly::class);
365 $formDataGroup->setProviderList([ TcaSelectItems::class ]);
366 /** @var FormDataCompiler $formDataCompiler */
367 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
368 $compilerResult = $formDataCompiler->compile($selectDataInput);
369 $selItems = $compilerResult['processedTca']['columns']['dummyField']['config']['items'];
370 } else {
371 $selItems = FALSE;
372 }
373 return $selItems;
374 }
375
376 /**
377 * Makes a flat array from the $possibleRecords array.
378 * The key of the flat array is the value of the record,
379 * the value of the flat array is the label of the record.
380 *
381 * @param array $possibleRecords The possibleRecords array (for select fields)
382 * @return mixed A flat array with key=uid, value=label; if $possibleRecords isn't an array, FALSE is returned.
383 */
384 protected function getPossibleRecordsFlat($possibleRecords) {
385 $flat = FALSE;
386 if (is_array($possibleRecords)) {
387 $flat = array();
388 foreach ($possibleRecords as $record) {
389 $flat[$record[1]] = $record[0];
390 }
391 }
392 return $flat;
393 }
394
395 /**
396 * Creates the HTML code of a general link to be used on a level of inline children.
397 * The possible keys for the parameter $type are 'newRecord', 'localize' and 'synchronize'.
398 *
399 * @param string $type The link type, values are 'newRecord', 'localize' and 'synchronize'.
400 * @param string $objectPrefix The "path" to the child record to create (e.g. 'data-parentPageId-partenTable-parentUid-parentField-childTable]')
401 * @param array $conf TCA configuration of the parent(!) field
402 * @return string The HTML code of the new link, wrapped in a div
403 */
404 protected function getLevelInteractionLink($type, $objectPrefix, $conf = array()) {
405 $languageService = $this->getLanguageService();
406 $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
407 $attributes = array();
408 switch ($type) {
409 case 'newRecord':
410 $title = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.createnew', TRUE);
411 $icon = 'actions-document-new';
412 $className = 'typo3-newRecordLink';
413 $attributes['class'] = 'btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'];
414 $attributes['onclick'] = 'return inline.createNewRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ')';
415 if (!empty($conf['inline']['inlineNewButtonStyle'])) {
416 $attributes['style'] = $conf['inline']['inlineNewButtonStyle'];
417 }
418 if (!empty($conf['appearance']['newRecordLinkAddTitle'])) {
419 $title = sprintf(
420 $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.createnew.link', TRUE),
421 $languageService->sL($GLOBALS['TCA'][$conf['foreign_table']]['ctrl']['title'], TRUE)
422 );
423 } elseif (isset($conf['appearance']['newRecordLinkTitle']) && $conf['appearance']['newRecordLinkTitle'] !== '') {
424 $title = $languageService->sL($conf['appearance']['newRecordLinkTitle'], TRUE);
425 }
426 break;
427 case 'localize':
428 $title = $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localizeAllRecords', TRUE);
429 $icon = 'actions-document-localize';
430 $className = 'typo3-localizationLink';
431 $attributes['class'] = 'btn btn-default';
432 $attributes['onclick'] = 'return inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($objectPrefix) . ', \'localize\')';
433 break;
434 case 'synchronize':
435 $title = $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:synchronizeWithOriginalLanguage', TRUE);
436 $icon = 'actions-document-synchronize';
437 $className = 'typo3-synchronizationLink';
438 $attributes['class'] = 'btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'];
439 $attributes['onclick'] = 'return inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($objectPrefix) . ', \'synchronize\')';
440 break;
441 default:
442 $title = '';
443 $icon = '';
444 $className = '';
445 }
446 // Create the link:
447 $icon = $icon ? $this->iconFactory->getIcon($icon, Icon::SIZE_SMALL)->render() : '';
448 $link = $this->wrapWithAnchor($icon . $title, '#', $attributes);
449 return '<div' . ($className ? ' class="' . $className . '"' : '') . 'title="' . $title . '">' . $link . '</div>';
450 }
451
452 /**
453 * Wraps a text with an anchor and returns the HTML representation.
454 *
455 * @param string $text The text to be wrapped by an anchor
456 * @param string $link The link to be used in the anchor
457 * @param array $attributes Array of attributes to be used in the anchor
458 * @return string The wrapped text as HTML representation
459 */
460 protected function wrapWithAnchor($text, $link, $attributes = array()) {
461 $link = trim($link);
462 $result = '<a href="' . ($link ?: '#') . '"';
463 foreach ($attributes as $key => $value) {
464 $result .= ' ' . $key . '="' . htmlspecialchars(trim($value)) . '"';
465 }
466 $result .= '>' . $text . '</a>';
467 return $result;
468 }
469
470 /**
471 * Get a selector as used for the select type, to select from all available
472 * records and to create a relation to the embedding record (e.g. like MM).
473 *
474 * @param array $selItems Array of all possible records
475 * @param array $conf TCA configuration of the parent(!) field
476 * @param array $uniqueIds The uids that have already been used and should be unique
477 * @return string A HTML <select> box with all possible records
478 */
479 protected function renderPossibleRecordsSelector($selItems, $conf, $uniqueIds = array()) {
480 $foreign_selector = $conf['foreign_selector'];
481 $selConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($conf, $foreign_selector);
482 $item = '';
483 if ($selConfig['type'] === 'select') {
484 $item = $this->renderPossibleRecordsSelectorTypeSelect($selItems, $conf, $selConfig['PA'], $uniqueIds);
485 } elseif ($selConfig['type'] === 'groupdb') {
486 $item = $this->renderPossibleRecordsSelectorTypeGroupDB($conf, $selConfig['PA']);
487 }
488 return $item;
489 }
490
491 /**
492 * Generate a link that opens an element browser in a new window.
493 * For group/db there is no way to use a "selector" like a <select>|</select>-box.
494 *
495 * @param array $conf TCA configuration of the parent(!) field
496 * @param array $PA An array with additional configuration options
497 * @return string A HTML link that opens an element browser in a new window
498 */
499 protected function renderPossibleRecordsSelectorTypeGroupDB($conf, &$PA) {
500 $backendUser = $this->getBackendUserAuthentication();
501 $languageService = $this->getLanguageService();
502
503 $config = $PA['fieldConf']['config'];
504 ArrayUtility::mergeRecursiveWithOverrule($config, $conf);
505 $foreign_table = $config['foreign_table'];
506 $allowed = $config['allowed'];
507 $objectPrefix = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']) . '-' . $foreign_table;
508 $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
509 $mode = 'db';
510 $showUpload = FALSE;
511 if (!empty($config['appearance']['createNewRelationLinkTitle'])) {
512 $createNewRelationText = $languageService->sL($config['appearance']['createNewRelationLinkTitle'], TRUE);
513 } else {
514 $createNewRelationText = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.createNewRelation', TRUE);
515 }
516 if (is_array($config['appearance'])) {
517 if (isset($config['appearance']['elementBrowserType'])) {
518 $mode = $config['appearance']['elementBrowserType'];
519 }
520 if ($mode === 'file') {
521 $showUpload = TRUE;
522 }
523 if (isset($config['appearance']['fileUploadAllowed'])) {
524 $showUpload = (bool)$config['appearance']['fileUploadAllowed'];
525 }
526 if (isset($config['appearance']['elementBrowserAllowed'])) {
527 $allowed = $config['appearance']['elementBrowserAllowed'];
528 }
529 }
530 $browserParams = '|||' . $allowed . '|' . $objectPrefix . '|inline.checkUniqueElement||inline.importElement';
531 $onClick = 'setFormValueOpenBrowser(' . GeneralUtility::quoteJSvalue($mode) . ', ' . GeneralUtility::quoteJSvalue($browserParams) . '); return false;';
532
533 $buttonStyle = '';
534 if (isset($config['inline']['inlineNewRelationButtonStyle'])) {
535 $buttonStyle = ' style="' . $config['inline']['inlineNewRelationButtonStyle'] . '"';
536 }
537
538 $item = '
539 <a href="#" class="btn btn-default inlineNewRelationButton ' . $this->inlineData['config'][$nameObject]['md5'] . '"
540 ' . $buttonStyle . ' onclick="' . htmlspecialchars($onClick) . '" title="' . $createNewRelationText . '">
541 ' . $this->iconFactory->getIcon('actions-insert-record', Icon::SIZE_SMALL)->render() . '
542 ' . $createNewRelationText . '
543 </a>';
544 $isDirectFileUploadEnabled = (bool)$this->getBackendUserAuthentication()->uc['edit_docModuleUpload'];
545 $allowedArray = GeneralUtility::trimExplode(',', $allowed, TRUE);
546 $onlineMediaAllowed = OnlineMediaHelperRegistry::getInstance()->getSupportedFileExtensions();
547 if (!empty($allowedArray)) {
548 $onlineMediaAllowed = array_intersect($allowedArray, $onlineMediaAllowed);
549 }
550 if ($showUpload && $isDirectFileUploadEnabled) {
551 $folder = $backendUser->getDefaultUploadFolder(
552 $this->data['parentPageRow']['uid'],
553 $this->data['tableName'],
554 $this->data['fieldName']
555 );
556 if (
557 $folder instanceof Folder
558 && $folder->checkActionPermission('add')
559 ) {
560 $maxFileSize = GeneralUtility::getMaxUploadFileSize() * 1024;
561 $item .= ' <a href="#" class="btn btn-default t3js-drag-uploader inlineNewFileUploadButton ' . $this->inlineData['config'][$nameObject]['md5'] . '"
562 ' . $buttonStyle . '
563 data-dropzone-target="#' . htmlspecialchars($this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid'])) . '"
564 data-insert-dropzone-before="1"
565 data-file-irre-object="' . htmlspecialchars($objectPrefix) . '"
566 data-file-allowed="' . htmlspecialchars($allowed) . '"
567 data-target-folder="' . htmlspecialchars($folder->getCombinedIdentifier()) . '"
568 data-max-file-size="' . htmlspecialchars($maxFileSize) . '"
569 ><span class="t3-icon t3-icon-actions t3-icon-actions-edit t3-icon-edit-upload">&nbsp;</span>';
570 $item .= $languageService->sL('LLL:EXT:lang/locallang_core.xlf:file_upload.select-and-submit', TRUE);
571 $item .= '</a>';
572
573 if (!empty($onlineMediaAllowed)) {
574 $buttonText = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:online_media.new_media.button', TRUE);
575 $placeholder = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:online_media.new_media.placeholder', TRUE);
576 $buttonSubmit = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:online_media.new_media.submit', TRUE);
577 $item .= '
578 <span class="btn btn-default t3js-online-media-add-btn"
579 data-file-irre-object="' . htmlspecialchars($objectPrefix) . '"
580 data-online-media-allowed="' . htmlspecialchars(implode(',', $onlineMediaAllowed)) . '"
581 data-target-folder="' . htmlspecialchars($folder->getCombinedIdentifier()) . '"
582 title="' . $buttonText . '"
583 data-btn-submit="' . $buttonSubmit . '"
584 data-placeholder="' . $placeholder . '"
585 >
586 '. $this->iconFactory->getIcon('actions-online-media-add', Icon::SIZE_SMALL)->render() . '
587 ' . $buttonText . '</span>';
588 }
589 }
590 }
591
592 $item = '<div class="form-control-wrap">' . $item . '</div>';
593 $allowedList = '';
594 $allowedLabel = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.allowedFileExtensions', TRUE);
595 foreach ($allowedArray as $allowedItem) {
596 $allowedList .= '<span class="label label-success">' . strtoupper($allowedItem) . '</span> ';
597 }
598 if (!empty($allowedList)) {
599 $item .= '<div class="help-block">' . $allowedLabel . '<br>' . $allowedList . '</div>';
600 }
601 $item = '<div class="form-group t3js-formengine-validation-marker">' . $item . '</div>';
602 return $item;
603 }
604
605 /**
606 * Get a selector as used for the select type, to select from all available
607 * records and to create a relation to the embedding record (e.g. like MM).
608 *
609 * @param array $selItems Array of all possible records
610 * @param array $conf TCA configuration of the parent(!) field
611 * @param array $PA An array with additional configuration options
612 * @param array $uniqueIds The uids that have already been used and should be unique
613 * @return string A HTML <select> box with all possible records
614 */
615 protected function renderPossibleRecordsSelectorTypeSelect($selItems, $conf, &$PA, $uniqueIds = array()) {
616 $foreign_table = $conf['foreign_table'];
617 $foreign_selector = $conf['foreign_selector'];
618 $PA = array();
619 $PA['fieldConf'] = $GLOBALS['TCA'][$foreign_table]['columns'][$foreign_selector];
620 $PA['fieldTSConfig'] = FormEngineUtility::getTSconfigForTableRow($foreign_table, array(), $foreign_selector);
621 $config = $PA['fieldConf']['config'];
622 $item = '';
623 // @todo $disabled is not present - should be read from config?
624 $disabled = FALSE;
625 if (!$disabled) {
626 $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);;
627 // Create option tags:
628 $opt = array();
629 foreach ($selItems as $p) {
630 if (!in_array($p[1], $uniqueIds)) {
631 $opt[] = '<option value="' . htmlspecialchars($p[1]) . '">' . htmlspecialchars($p[0]) . '</option>';
632 }
633 }
634 // Put together the selector box:
635 $itemListStyle = isset($config['itemListStyle']) ? ' style="' . htmlspecialchars($config['itemListStyle']) . '"' : '';
636 $size = (int)$conf['size'];
637 $size = $conf['autoSizeMax'] ? MathUtility::forceIntegerInRange(count($selItems) + 1, MathUtility::forceIntegerInRange($size, 1), $conf['autoSizeMax']) : $size;
638 $onChange = 'return inline.importNewRecord(' . GeneralUtility::quoteJSvalue($nameObject . '-' . $conf['foreign_table']) . ')';
639 $item = '
640 <select id="' . $nameObject . '-' . $conf['foreign_table'] . '_selector" class="form-control"' . ($size ? ' size="' . $size . '"' : '') . ' onchange="' . htmlspecialchars($onChange) . '"' . $PA['onFocus'] . $itemListStyle . ($conf['foreign_unique'] ? ' isunique="isunique"' : '') . '>
641 ' . implode('', $opt) . '
642 </select>';
643
644 if ($size <= 1) {
645 // Add a "Create new relation" link for adding new relations
646 // This is necessary, if the size of the selector is "1" or if
647 // there is only one record item in the select-box, that is selected by default
648 // The selector-box creates a new relation on using an onChange event (see some line above)
649 if (!empty($conf['appearance']['createNewRelationLinkTitle'])) {
650 $createNewRelationText = $this->getLanguageService()->sL($conf['appearance']['createNewRelationLinkTitle'], TRUE);
651 } else {
652 $createNewRelationText = $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:cm.createNewRelation', TRUE);
653 }
654 $item .= '
655 <span class="input-group-btn">
656 <a href="#" class="btn btn-default" onclick="' . htmlspecialchars($onChange) . '" title="' . $createNewRelationText .'">
657 ' . $this->iconFactory->getIcon('actions-document-new', Icon::SIZE_SMALL)->render() . $createNewRelationText . '
658 </a>
659 </span>';
660 } else {
661 $item .= '
662 <span class="input-group-btn btn"></span>';
663 }
664
665 // Wrap the selector and add a spacer to the bottom
666
667 $item = '<div class="input-group form-group t3js-formengine-validation-marker ' . $this->inlineData['config'][$nameObject]['md5'] . '">' . $item . '</div>';
668 }
669 return $item;
670 }
671
672 /**
673 * Extracts FlexForm parts of a form element name like
674 * data[table][uid][field][sDEF][lDEF][FlexForm][vDEF]
675 * Helper method used in inline
676 *
677 * @param string $formElementName The form element name
678 * @return array|NULL
679 */
680 protected function extractFlexFormParts($formElementName) {
681 $flexFormParts = NULL;
682 $matches = array();
683 if (preg_match('#^data(?:\[[^]]+\]){3}(\[data\](?:\[[^]]+\]){4,})$#', $formElementName, $matches)) {
684 $flexFormParts = GeneralUtility::trimExplode(
685 '][',
686 trim($matches[1], '[]')
687 );
688 }
689 return $flexFormParts;
690 }
691
692 /**
693 * @return BackendUserAuthentication
694 */
695 protected function getBackendUserAuthentication() {
696 return $GLOBALS['BE_USER'];
697 }
698
699 /**
700 * @return LanguageService
701 */
702 protected function getLanguageService() {
703 return $GLOBALS['LANG'];
704 }
705
706 }