/** * Convert the DOM object-id of an inline container to an array. * The object-id could look like 'data-parentPageId-tx_mmftest_company-1-employees'. * This initializes $this->inlineStructure - used by AJAX entry points * There are two keys: * - 'stable': Containing full qualified identifiers (table, uid and field) * - 'unstable': Containing partly filled data (e.g. only table and possibly field) * * @param string $domObjectId The DOM object-id * @param bool $loadConfig Load the TCA configuration for that level (default: TRUE) * @return void */ public function initializeByParsingDomObjectIdString($domObjectId, $loadConfig = TRUE) { $unstable = array(); $vector = array('table', 'uid', 'field'); // Substitute FlexForm addition and make parsing a bit easier $domObjectId = str_replace('---', ':', $domObjectId); // The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>) $pattern = '/^data' . '-' . '(.+?)' . '-' . '(.+)$/'; if (preg_match($pattern, $domObjectId, $match)) { $inlineFirstPid = $match[1]; $parts = explode('-', $match[2]); $partsCnt = count($parts); for ($i = 0; $i < $partsCnt; $i++) { if ($i > 0 && $i % 3 == 0) { // Load the TCA configuration of the table field and store it in the stack if ($loadConfig) { $unstable['config'] = $GLOBALS['TCA'][$unstable['table']]['columns'][$unstable['field']]['config']; // Fetch TSconfig: $TSconfig = FormEngineUtility::getTSconfigForTableRow($unstable['table'], array('uid' => $unstable['uid'], 'pid' => $inlineFirstPid), $unstable['field']); // Override TCA field config by TSconfig: if (!$TSconfig['disabled']) { $unstable['config'] = FormEngineUtility::overrideFieldConf($unstable['config'], $TSconfig); } $unstable['localizationMode'] = BackendUtility::getInlineLocalizationMode($unstable['table'], $unstable['config']); } // Extract FlexForm from field part (if any) if (strpos($unstable['field'], ':') !== FALSE) { $fieldParts = GeneralUtility::trimExplode(':', $unstable['field']); $unstable['field'] = array_shift($fieldParts); // FlexForm parts start with data: if (count($fieldParts) > 0 && $fieldParts[0] === 'data') { $unstable['flexform'] = $fieldParts; } } $this->inlineStructure['stable'][] = $unstable; $unstable = array(); } $unstable[$vector[$i % 3]] = $parts[$i]; } if (count($unstable)) { $this->inlineStructure['unstable'] = $unstable; } } }
/** * Entry method * * @return array As defined in initializeResultArray() of AbstractNode */ public function render() { $backendUser = $this->getBackendUserAuthentication(); $languageService = $this->getLanguageService(); $resultArray = $this->initializeResultArray(); $table = $this->data['tableName']; $row = $this->data['databaseRow']; $fieldName = $this->data['fieldName']; // @todo: it should be safe at this point, this array exists ... if (!is_array($this->data['processedTca']['columns'][$fieldName])) { return $resultArray; } $parameterArray = array(); $parameterArray['fieldConf'] = $this->data['processedTca']['columns'][$fieldName]; $isOverlay = false; // This field decides whether the current record is an overlay (as opposed to being a standalone record) // Based on this decision we need to trigger field exclusion or special rendering (like readOnly) if (isset($this->data['processedTca']['ctrl']['transOrigPointerField']) && is_array($this->data['processedTca']['columns'][$this->data['processedTca']['ctrl']['transOrigPointerField']]) && is_array($row[$this->data['processedTca']['ctrl']['transOrigPointerField']]) && $row[$this->data['processedTca']['ctrl']['transOrigPointerField']][0] > 0) { $isOverlay = true; } // A couple of early returns in case the field should not be rendered // Check if this field is configured and editable according to exclude fields and other configuration if ($parameterArray['fieldConf']['exclude'] && !$backendUser->check('non_exclude_fields', $table . ':' . $fieldName) || $parameterArray['fieldConf']['config']['type'] === 'passthrough' || !$backendUser->isRTE() && $parameterArray['fieldConf']['config']['showIfRTE'] || $isOverlay && !$parameterArray['fieldConf']['l10n_display'] && $parameterArray['fieldConf']['l10n_mode'] === 'exclude' || $isOverlay && $this->data['localizationMode'] && $this->data['localizationMode'] !== $parameterArray['fieldConf']['l10n_cat'] || $this->inlineFieldShouldBeSkipped()) { return $resultArray; } $parameterArray['fieldTSConfig'] = []; if (isset($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']) && is_array($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'])) { $parameterArray['fieldTSConfig'] = $this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']; } if ($parameterArray['fieldTSConfig']['disabled']) { return $resultArray; } // Override fieldConf by fieldTSconfig: $parameterArray['fieldConf']['config'] = FormEngineUtility::overrideFieldConf($parameterArray['fieldConf']['config'], $parameterArray['fieldTSConfig']); $parameterArray['itemFormElName'] = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']'; $parameterArray['itemFormElID'] = 'data_' . $table . '_' . $row['uid'] . '_' . $fieldName; $newElementBaseName = $this->data['elementBaseName'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']'; // The value to show in the form field. $parameterArray['itemFormElValue'] = $row[$fieldName]; // Set field to read-only if configured for translated records to show default language content as readonly if ($parameterArray['fieldConf']['l10n_display'] && GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'defaultAsReadonly') && $isOverlay) { $parameterArray['fieldConf']['config']['readOnly'] = true; $parameterArray['itemFormElValue'] = $this->data['defaultLanguageRow'][$fieldName]; } if (strpos($this->data['processedTca']['ctrl']['type'], ':') === false) { $typeField = $this->data['processedTca']['ctrl']['type']; } else { $typeField = substr($this->data['processedTca']['ctrl']['type'], 0, strpos($this->data['processedTca']['ctrl']['type'], ':')); } // Create a JavaScript code line which will ask the user to save/update the form due to changing the element. // This is used for eg. "type" fields and others configured with "requestUpdate" if (!empty($this->data['processedTca']['ctrl']['type']) && $fieldName === $typeField || !empty($this->data['processedTca']['ctrl']['requestUpdate']) && GeneralUtility::inList(str_replace(' ', '', $this->data['processedTca']['ctrl']['requestUpdate']), $fieldName)) { if ($backendUser->jsConfirmation(JsConfirmation::TYPE_CHANGE)) { $alertMsgOnChange = 'top.TYPO3.Modal.confirm(TBE_EDITOR.labels.refreshRequired.title, TBE_EDITOR.labels.refreshRequired.content).on("button.clicked", function(e) { if (e.target.name == "ok" && TBE_EDITOR.checkSubmit(-1)) { TBE_EDITOR.submitForm() } top.TYPO3.Modal.dismiss(); });'; } else { $alertMsgOnChange = 'if (TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm() };'; } } else { $alertMsgOnChange = ''; } // JavaScript code for event handlers: $parameterArray['fieldChangeFunc'] = array(); $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = 'TBE_EDITOR.fieldChanged(' . GeneralUtility::quoteJSvalue($table) . ',' . GeneralUtility::quoteJSvalue($row['uid']) . ',' . GeneralUtility::quoteJSvalue($fieldName) . ',' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ');'; $parameterArray['fieldChangeFunc']['alert'] = $alertMsgOnChange; // If this is the child of an inline type and it is the field creating the label if ($this->isInlineChildAndLabelField($table, $fieldName)) { /** @var InlineStackProcessor $inlineStackProcessor */ $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class); $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']); $inlineDomObjectId = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']); $inlineObjectId = implode('-', array($inlineDomObjectId, $table, $row['uid'])); $parameterArray['fieldChangeFunc']['inline'] = 'inline.handleChangedField(' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ',' . GeneralUtility::quoteJSvalue($inlineObjectId) . ');'; } // Based on the type of the item, call a render function on a child element $options = $this->data; $options['parameterArray'] = $parameterArray; $options['elementBaseName'] = $newElementBaseName; if (!empty($parameterArray['fieldConf']['config']['renderType'])) { $options['renderType'] = $parameterArray['fieldConf']['config']['renderType']; } else { // Fallback to type if no renderType is given $options['renderType'] = $parameterArray['fieldConf']['config']['type']; } $resultArray = $this->nodeFactory->create($options)->render(); // If output is empty stop further processing. // This means there was internal processing only and we don't need to add additional information if (empty($resultArray['html'])) { return $resultArray; } $html = $resultArray['html']; // @todo: the language handling, the null and the placeholder stuff should be embedded in the single // @todo: element classes. Basically, this method should return here and have the element classes // @todo: decide on language stuff and other wraps already. // Add language + diff $renderLanguageDiff = true; if ($parameterArray['fieldConf']['l10n_display'] && (GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'hideDiff') || GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'defaultAsReadonly'))) { $renderLanguageDiff = false; } if ($renderLanguageDiff) { $html = $this->renderDefaultLanguageContent($table, $fieldName, $row, $html); $html = $this->renderDefaultLanguageDiff($table, $fieldName, $row, $html); } $fieldItemClasses = array('t3js-formengine-field-item'); // NULL value and placeholder handling $nullControlNameAttribute = ' name="' . htmlspecialchars('control[active][' . $table . '][' . $row['uid'] . '][' . $fieldName . ']') . '"'; if (!empty($parameterArray['fieldConf']['config']['eval']) && GeneralUtility::inList($parameterArray['fieldConf']['config']['eval'], 'null') && (empty($parameterArray['fieldConf']['config']['mode']) || $parameterArray['fieldConf']['config']['mode'] !== 'useOrOverridePlaceholder')) { // This field has eval=null set, but has no useOverridePlaceholder defined. // Goal is to have a field that can distinct between NULL and empty string in the database. // A checkbox and an additional hidden field will be created, both with the same name // and prefixed with "control[active]". If the checkbox is set (value 1), the value from the casual // input field will be written to the database. If the checkbox is not set, the hidden field // transfers value=0 to DataHandler, the value of the input field will then be reset to NULL by the // DataHandler at an early point in processing, so NULL will be written to DB as field value. // If the value of the field *is* NULL at the moment, an additional class is set // @todo: This does not work well at the moment, but is kept for now. see input_14 of ext:styleguide as example $checked = ' checked="checked"'; if ($this->data['databaseRow'][$fieldName] === null) { $fieldItemClasses[] = 'disabled'; $checked = ''; } $formElementName = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']'; $onChange = htmlspecialchars('typo3form.fieldSetNull(' . GeneralUtility::quoteJSvalue($formElementName) . ', !this.checked)'); $nullValueWrap = array(); $nullValueWrap[] = '<div class="' . implode(' ', $fieldItemClasses) . '">'; $nullValueWrap[] = '<div class="t3-form-field-disable"></div>'; $nullValueWrap[] = '<div class="checkbox">'; $nullValueWrap[] = '<label>'; $nullValueWrap[] = '<input type="hidden"' . $nullControlNameAttribute . ' value="0" />'; $nullValueWrap[] = '<input type="checkbox"' . $nullControlNameAttribute . ' value="1" onchange="' . $onChange . '"' . $checked . ' /> '; $nullValueWrap[] = '</label>'; $nullValueWrap[] = $html; $nullValueWrap[] = '</div>'; $nullValueWrap[] = '</div>'; $html = implode(LF, $nullValueWrap); } elseif (isset($parameterArray['fieldConf']['config']['mode']) && $parameterArray['fieldConf']['config']['mode'] === 'useOrOverridePlaceholder') { // This field has useOverridePlaceholder set. // Here, a value from a deeper DB structure can be "fetched up" as value, and can also be overridden by a // local value. This is used in FAL, where eg. the "title" field can have the default value from sys_file_metadata, // the title field of sys_file_reference is then set to NULL. Or the "override" checkbox is set, and a string // or an empty string is then written to the field of sys_file_reference. // The situation is similar to the NULL handling above, but additionally a "default" value should be shown. // To achieve this, again a hidden control[hidden] field is added together with a checkbox with the same name // to transfer the information whether the default value should be used or not: Checkbox checked transfers 1 as // value in control[active], meaning the overridden value should be used. // Additionally to the casual input field, a second field is added containing the "placeholder" value. This // field has no name attribute and is not transferred at all. Those two are then hidden / shown depending // on the state of the above checkbox in via JS. $placeholder = empty($parameterArray['fieldConf']['config']['placeholder']) ? '' : $parameterArray['fieldConf']['config']['placeholder']; $onChange = 'typo3form.fieldTogglePlaceholder(' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ', !this.checked)'; $checked = $parameterArray['itemFormElValue'] === null ? '' : ' checked="checked"'; $resultArray['additionalJavaScriptPost'][] = 'typo3form.fieldTogglePlaceholder(' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ', ' . ($checked ? 'false' : 'true') . ');'; // Renders an input or textarea field depending on type of "parent" $options = array(); $options['databaseRow'] = array(); $options['table'] = ''; $options['parameterArray'] = $parameterArray; $options['parameterArray']['itemFormElValue'] = GeneralUtility::fixed_lgd_cs($placeholder, 30); $options['renderType'] = 'none'; $noneElementResult = $this->nodeFactory->create($options)->render(); $noneElementHtml = $noneElementResult['html']; $placeholderWrap = array(); $placeholderWrap[] = '<div class="' . implode(' ', $fieldItemClasses) . '">'; $placeholderWrap[] = '<div class="t3-form-field-disable"></div>'; $placeholderWrap[] = '<div class="checkbox">'; $placeholderWrap[] = '<label>'; $placeholderWrap[] = '<input type="hidden"' . $nullControlNameAttribute . ' value="0" />'; $placeholderWrap[] = '<input type="checkbox"' . $nullControlNameAttribute . ' value="1" id="tce-forms-textfield-use-override-' . $fieldName . '-' . $row['uid'] . '" onchange="' . htmlspecialchars($onChange) . '"' . $checked . ' />'; $placeholderWrap[] = sprintf($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.placeholder.override'), BackendUtility::getRecordTitlePrep($placeholder, 20)); $placeholderWrap[] = '</label>'; $placeholderWrap[] = '</div>'; $placeholderWrap[] = '<div class="t3js-formengine-placeholder-placeholder">'; $placeholderWrap[] = $noneElementHtml; $placeholderWrap[] = '</div>'; $placeholderWrap[] = '<div class="t3js-formengine-placeholder-formfield">'; $placeholderWrap[] = $html; $placeholderWrap[] = '</div>'; $placeholderWrap[] = '</div>'; $html = implode(LF, $placeholderWrap); } elseif ($parameterArray['fieldConf']['config']['type'] !== 'user' || empty($parameterArray['fieldConf']['config']['noTableWrapping'])) { // Add a casual wrap if the field is not of type user with no wrap requested. $standardWrap = array(); $standardWrap[] = '<div class="' . implode(' ', $fieldItemClasses) . '">'; $standardWrap[] = '<div class="t3-form-field-disable"></div>'; $standardWrap[] = $html; $standardWrap[] = '</div>'; $html = implode(LF, $standardWrap); } $resultArray['html'] = $html; return $resultArray; }
/** * If the select field is build by a foreign_table the related UIDs * will be returned. * * Otherwise the label of the currently selected value will be written * to the alternativeFieldValue class property. * * @param array $fieldConfig The "config" section of the TCA for the current select field. * @param string $fieldName The name of the select field. * @param string $value The current value in the local record, usually a comma separated list of selected values. * @return array Array of related UIDs. */ protected function getRelatedSelectFieldUids(array $fieldConfig, $fieldName, $value) { $relatedUids = array(); $isTraversable = FALSE; if (isset($fieldConfig['foreign_table'])) { $isTraversable = TRUE; // if a foreign_table is used we pre-filter the records for performance $fieldConfig['foreign_table_where'] .= ' AND ' . $fieldConfig['foreign_table'] . '.uid IN (' . $value . ')'; } $PA = array(); $PA['fieldConf']['config'] = $fieldConfig; $PA['fieldTSConfig'] = FormEngineUtility::getTSconfigForTableRow($this->currentTable, $this->currentRow, $fieldName); $PA['fieldConf']['config'] = FormEngineUtility::overrideFieldConf($PA['fieldConf']['config'], $PA['fieldTSConfig']); $selectItemArray = FormEngineUtility::getSelectItems($this->currentTable, $fieldName, $this->currentRow, $PA); if ($isTraversable && !empty($selectItemArray)) { $this->currentTable = $fieldConfig['foreign_table']; $relatedUids = $this->getSelectedValuesFromSelectItemArray($selectItemArray, $value); } else { $selectedLabels = $this->getSelectedValuesFromSelectItemArray($selectItemArray, $value, 1, TRUE); if (count($selectedLabels) === 1) { $this->alternativeFieldValue = $selectedLabels[0]; $this->forceAlternativeFieldValueUse = TRUE; } } return $relatedUids; }