/** * Handle AJAX calls to show a new inline-record of the given table. * * @param string $domObjectId The calling object in hierarchy, that requested a new record. * @param string|int $foreignUid If set, the new record should be inserted after that one. * @return array An array to be used for JSON */ protected function renderInlineNewChildRecord($domObjectId, $foreignUid) { // The current table - for this table we should add/import records $current = $this->inlineStackProcessor->getUnstableStructure(); // The parent table - this table embeds the current table $parent = $this->inlineStackProcessor->getStructureLevel(-1); $config = $parent['config']; if (empty($config['foreign_table']) || !is_array($GLOBALS['TCA'][$config['foreign_table']])) { return $this->getErrorMessageForAJAX('Wrong configuration in table ' . $parent['table']); } $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class); $config = FormEngineUtility::mergeInlineConfiguration($config); $collapseAll = isset($config['appearance']['collapseAll']) && $config['appearance']['collapseAll']; $expandSingle = isset($config['appearance']['expandSingle']) && $config['appearance']['expandSingle']; $inlineFirstPid = FormEngineUtility::getInlineFirstPidFromDomObjectId($domObjectId); // Dynamically create a new record using \TYPO3\CMS\Backend\Form\DataPreprocessor if (!$foreignUid || !MathUtility::canBeInterpretedAsInteger($foreignUid) || $config['foreign_selector']) { $record = $inlineRelatedRecordResolver->getNewRecord($inlineFirstPid, $current['table']); // Set default values for new created records if (isset($config['foreign_record_defaults']) && is_array($config['foreign_record_defaults'])) { $foreignTableConfig = $GLOBALS['TCA'][$current['table']]; // The following system relevant fields can't be set by foreign_record_defaults $notSettableFields = array('uid', 'pid', 't3ver_oid', 't3ver_id', 't3ver_label', 't3ver_wsid', 't3ver_state', 't3ver_stage', 't3ver_count', 't3ver_tstamp', 't3ver_move_id'); $configurationKeysForNotSettableFields = array('crdate', 'cruser_id', 'delete', 'origUid', 'transOrigDiffSourceField', 'transOrigPointerField', 'tstamp'); foreach ($configurationKeysForNotSettableFields as $configurationKey) { if (isset($foreignTableConfig['ctrl'][$configurationKey])) { $notSettableFields[] = $foreignTableConfig['ctrl'][$configurationKey]; } } foreach ($config['foreign_record_defaults'] as $fieldName => $defaultValue) { if (isset($foreignTableConfig['columns'][$fieldName]) && !in_array($fieldName, $notSettableFields)) { $record[$fieldName] = $defaultValue; } } } // Set language of new child record to the language of the parent record: if ($parent['localizationMode'] === 'select') { $parentRecord = $inlineRelatedRecordResolver->getRecord($parent['table'], $parent['uid']); $parentLanguageField = $GLOBALS['TCA'][$parent['table']]['ctrl']['languageField']; $childLanguageField = $GLOBALS['TCA'][$current['table']]['ctrl']['languageField']; if ($parentRecord[$parentLanguageField] > 0) { $record[$childLanguageField] = $parentRecord[$parentLanguageField]; } } } else { // @todo: Check this: Else also hits if $foreignUid = 0? $record = $inlineRelatedRecordResolver->getRecord($current['table'], $foreignUid); } // Now there is a foreign_selector, so there is a new record on the intermediate table, but // this intermediate table holds a field, which is responsible for the foreign_selector, so // we have to set this field to the uid we get - or if none, to a new uid if ($config['foreign_selector'] && $foreignUid) { $selConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($config, $config['foreign_selector']); // For a selector of type group/db, prepend the tablename (<tablename>_<uid>): $record[$config['foreign_selector']] = $selConfig['type'] != 'groupdb' ? '' : $selConfig['table'] . '_'; $record[$config['foreign_selector']] .= $foreignUid; if ($selConfig['table'] === 'sys_file') { $fileRecord = $inlineRelatedRecordResolver->getRecord($selConfig['table'], $foreignUid); if ($fileRecord !== FALSE && !$this->checkInlineFileTypeAccessForField($selConfig, $fileRecord)) { return $this->getErrorMessageForAJAX('File extension ' . $fileRecord['extension'] . ' is not allowed here!'); } } } // The HTML-object-id's prefix of the dynamically created record $objectName = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid); $objectPrefix = $objectName . '-' . $current['table']; $objectId = $objectPrefix . '-' . $record['uid']; $options = $this->getConfigurationOptionsForChildElements(); $options['databaseRow'] = array('uid' => $parent['uid']); $options['inlineFirstPid'] = $inlineFirstPid; $options['inlineRelatedRecordToRender'] = $record; $options['inlineRelatedRecordConfig'] = $config; $options['inlineStructure'] = $this->inlineStackProcessor->getStructure(); $options['isAjaxContext'] = TRUE; $options['renderType'] = 'inlineRecordContainer'; $childArray = $this->nodeFactory->create($options)->render(); if ($childArray === FALSE) { return $this->getErrorMessageForAJAX('Access denied'); } $this->mergeResult($childArray); $jsonArray = array('data' => $childArray['html'], 'scriptCall' => array()); if (!$current['uid']) { $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);'; $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($record['uid']) . ',null,' . GeneralUtility::quoteJSvalue($foreignUid) . ');'; } else { $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);'; $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($record['uid']) . ',' . GeneralUtility::quoteJSvalue($current['uid']) . ',' . GeneralUtility::quoteJSvalue($foreignUid) . ');'; } $jsonArray = $this->getInlineAjaxCommonScriptCalls($jsonArray, $config, $inlineFirstPid); // Collapse all other records if requested: if (!$collapseAll && $expandSingle) { $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ', ' . GeneralUtility::quoteJSvalue($objectPrefix) . ', ' . GeneralUtility::quoteJSvalue($record['uid']) . ');'; } // Tell the browser to scroll to the newly created record $jsonArray['scriptCall'][] = 'Element.scrollTo(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');'; // Fade out and fade in the new record in the browser view to catch the user's eye $jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');'; return $jsonArray; }
/** * Entry method * * @return array As defined in initializeResultArray() of AbstractNode */ public function render() { $languageService = $this->getLanguageService(); $this->inlineData = $this->globalOptions['inlineData']; /** @var InlineStackProcessor $inlineStackProcessor */ $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class); $this->inlineStackProcessor = $inlineStackProcessor; $inlineStackProcessor->initializeByGivenStructure($this->globalOptions['inlineStructure']); $table = $this->globalOptions['table']; $row = $this->globalOptions['databaseRow']; $field = $this->globalOptions['fieldName']; $parameterArray = $this->globalOptions['parameterArray']; $resultArray = $this->initializeResultArray(); $html = ''; // An inline field must have a foreign_table, if not, stop all further inline actions for this field if (!$parameterArray['fieldConf']['config']['foreign_table'] || !is_array($GLOBALS['TCA'][$parameterArray['fieldConf']['config']['foreign_table']])) { return $resultArray; } $config = FormEngineUtility::mergeInlineConfiguration($parameterArray['fieldConf']['config']); $foreign_table = $config['foreign_table']; $language = 0; if (BackendUtility::isTableLocalizable($table)) { $language = (int) $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]; } $minItems = MathUtility::forceIntegerInRange($config['minitems'], 0); $maxItems = MathUtility::forceIntegerInRange($config['maxitems'], 0); if (!$maxItems) { $maxItems = 100000; } // Add the current inline job to the structure stack $newStructureItem = array('table' => $table, 'uid' => $row['uid'], 'field' => $field, 'config' => $config, 'localizationMode' => BackendUtility::getInlineLocalizationMode($table, $config)); // Extract FlexForm parts (if any) from element name, e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF') if (!empty($parameterArray['itemFormElName'])) { $flexFormParts = FormEngineUtility::extractFlexFormParts($parameterArray['itemFormElName']); if ($flexFormParts !== NULL) { $newStructureItem['flexform'] = $flexFormParts; } } $inlineStackProcessor->pushStableStructureItem($newStructureItem); // e.g. data[<table>][<uid>][<field>] $nameForm = $inlineStackProcessor->getCurrentStructureFormPrefix(); // e.g. data-<pid>-<table1>-<uid1>-<field1>-<table2>-<uid2>-<field2> $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->globalOptions['inlineFirstPid']); // Get the records related to this inline record $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class); $relatedRecords = $inlineRelatedRecordResolver->getRelatedRecords($table, $field, $row, $parameterArray, $config, $this->globalOptions['inlineFirstPid']); // Set the first and last record to the config array $relatedRecordsUids = array_keys($relatedRecords['records']); $config['inline']['first'] = reset($relatedRecordsUids); $config['inline']['last'] = end($relatedRecordsUids); $top = $inlineStackProcessor->getStructureLevel(0); $this->inlineData['config'][$nameObject] = array('table' => $foreign_table, 'md5' => md5($nameObject)); $this->inlineData['config'][$nameObject . '-' . $foreign_table] = array('min' => $minItems, 'max' => $maxItems, 'sortable' => $config['appearance']['useSortable'], 'top' => array('table' => $top['table'], 'uid' => $top['uid']), 'context' => array('config' => $config, 'hmac' => GeneralUtility::hmac(serialize($config)))); $this->inlineData['nested'][$nameObject] = $this->globalOptions['tabAndInlineStack']; // If relations are required to be unique, get the uids that have already been used on the foreign side of the relation if ($config['foreign_unique']) { // If uniqueness *and* selector are set, they should point to the same field - so, get the configuration of one: $selConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($config, $config['foreign_unique']); // Get the used unique ids: $uniqueIds = $this->getUniqueIds($relatedRecords['records'], $config, $selConfig['type'] == 'groupdb'); $possibleRecords = $this->getPossibleRecords($table, $field, $row, $config, 'foreign_unique'); $uniqueMax = $config['appearance']['useCombination'] || $possibleRecords === FALSE ? -1 : count($possibleRecords); $this->inlineData['unique'][$nameObject . '-' . $foreign_table] = array('max' => $uniqueMax, 'used' => $uniqueIds, 'type' => $selConfig['type'], 'table' => $config['foreign_table'], 'elTable' => $selConfig['table'], 'field' => $config['foreign_unique'], 'selector' => $selConfig['selector'], 'possible' => $this->getPossibleRecordsFlat($possibleRecords)); } $resultArray['inlineData'] = $this->inlineData; // Render the localization links $localizationLinks = ''; if ($language > 0 && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0 && MathUtility::canBeInterpretedAsInteger($row['uid'])) { // Add the "Localize all records" link before all child records: if (isset($config['appearance']['showAllLocalizationLink']) && $config['appearance']['showAllLocalizationLink']) { $localizationLinks .= ' ' . $this->getLevelInteractionLink('localize', $nameObject . '-' . $foreign_table, $config); } // Add the "Synchronize with default language" link before all child records: if (isset($config['appearance']['showSynchronizationLink']) && $config['appearance']['showSynchronizationLink']) { $localizationLinks .= ' ' . $this->getLevelInteractionLink('synchronize', $nameObject . '-' . $foreign_table, $config); } } // Define how to show the "Create new record" link - if there are more than maxitems, hide it if ($relatedRecords['count'] >= $maxItems || $uniqueMax > 0 && $relatedRecords['count'] >= $uniqueMax) { $config['inline']['inlineNewButtonStyle'] = 'display: none;'; $config['inline']['inlineNewRelationButtonStyle'] = 'display: none;'; } // Render the level links (create new record): $levelLinks = $this->getLevelInteractionLink('newRecord', $nameObject . '-' . $foreign_table, $config); // Wrap all inline fields of a record with a <div> (like a container) $html .= '<div class="form-group" id="' . $nameObject . '">'; // Add the level links before all child records: if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'top') { $html .= '<div class="form-group t3js-formengine-validation-marker">' . $levelLinks . $localizationLinks . '</div>'; } // If it's required to select from possible child records (reusable children), add a selector box if ($config['foreign_selector'] && $config['appearance']['showPossibleRecordsSelector'] !== FALSE) { // If not already set by the foreign_unique, set the possibleRecords here and the uniqueIds to an empty array if (!$config['foreign_unique']) { $possibleRecords = $this->getPossibleRecords($table, $field, $row, $config); $uniqueIds = array(); } $selectorBox = $this->renderPossibleRecordsSelector($possibleRecords, $config, $uniqueIds); $html .= $selectorBox . $localizationLinks; } $title = $languageService->sL($parameterArray['fieldConf']['label']); $html .= '<div class="panel-group panel-hover" data-title="' . htmlspecialchars($title) . '" id="' . $nameObject . '_records">'; $relationList = array(); if (!empty($relatedRecords['records'])) { foreach ($relatedRecords['records'] as $rec) { $options = $this->globalOptions; $options['inlineRelatedRecordToRender'] = $rec; $options['inlineRelatedRecordConfig'] = $config; $options['inlineData'] = $this->inlineData; $options['inlineStructure'] = $inlineStackProcessor->getStructure(); $options['renderType'] = 'inlineRecordContainer'; /** @var NodeFactory $nodeFactory */ $nodeFactory = $this->globalOptions['nodeFactory']; $childArray = $nodeFactory->create($options)->render(); $html .= $childArray['html']; $childArray['html'] = ''; $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray); if (!isset($rec['__virtual']) || !$rec['__virtual']) { $relationList[] = $rec['uid']; } } } $html .= '</div>'; // Add the level links after all child records: if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'bottom') { $html .= $levelLinks . $localizationLinks; } if (is_array($config['customControls'])) { $html .= '<div id="' . $nameObject . '_customControls">'; foreach ($config['customControls'] as $customControlConfig) { $parameters = array('table' => $table, 'field' => $field, 'row' => $row, 'nameObject' => $nameObject, 'nameForm' => $nameForm, 'config' => $config); $html .= GeneralUtility::callUserFunction($customControlConfig, $parameters, $this); } $html .= '</div>'; } // Add Drag&Drop functions for sorting to FormEngine::$additionalJS_post if (count($relationList) > 1 && $config['appearance']['useSortable']) { $resultArray['additionalJavaScriptPost'][] = 'inline.createDragAndDropSorting("' . $nameObject . '_records' . '");'; } // Publish the uids of the child records in the given order to the browser $html .= '<input type="hidden" name="' . $nameForm . '" value="' . implode(',', $relationList) . '" ' . $this->getValidationDataAsDataAttribute(array('type' => 'inline', 'minitems' => $minItems, 'maxitems' => $maxItems)) . ' class="inlineRecord" />'; // Close the wrap for all inline fields (container) $html .= '</div>'; $resultArray['html'] = $html; return $resultArray; }