[!!!][TASK] Improve flex and TCA handling in FormEngine 79/50879/49
authorChristian Kuhn <lolli@schwarzbu.ch>
Sat, 3 Dec 2016 20:59:52 +0000 (21:59 +0100)
committerAnja Leichsenring <aleichsenring@ab-softlab.de>
Tue, 3 Jan 2017 13:33:10 +0000 (14:33 +0100)
The patch adapts a series of nasty form engine areas to more solid
code. The evaluate condition code is rewritten and works much better
in flex form scenarios. The suggest wizard and svg tree are much
more solid in flex forms. The group element is rewritten
towards a better readable and easier to refactor code, dropping
method dbFileIcons(). A bunch of issues is resolved along the way.

* TCA "default" now works in flex form section container elements
* The "displayCond" parser is now strict and throws exceptions on
  invalid syntax and wrong referenced fields to help debugging
  faulty display conditions
* TCA displayCond on flex fields can now be prefixed with the
  sheet name and can reference field values from neighbor sheets
* TCA displayCond now works with flex section containers
* TCA flex section container now throw an exception if select or
  group fields configure a MM relation - this is not supported
* TCA ctrl requestUpdate field is dropped, onChange=reload is now allowed
  not only on flex form fields, but also on normal columns fields
* TCA tree now works as section container element and initializes
  correctly on new records and new containers
* GroupElement rewrite to drop dbFileIcons()
* config option maxitems now optional for type=group and type=select
  and defaults to "many items allowed"
* inline now works in "fancy" flex situations with "new" records
  by handing the final dataStructureIdentifier around
* FormEngine no longer loads extJS

Change-Id: Id1d081627529cc1502bb198389e5bd69372815cd
Resolves: #78899
Resolves: #72307
Resolves: #75646
Resolves: #76637
Resolves: #72106
Resolves: #78824
Resolves: #76793
Resolves: #68247
Resolves: #69715
Related: #78460
Related: #67198
Related: #72294
Releases: master
Reviewed-on: https://review.typo3.org/50879
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
91 files changed:
typo3/sysext/backend/Classes/Controller/FormFlexAjaxController.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Controller/FormInlineAjaxController.php
typo3/sysext/backend/Classes/Controller/FormSelectTreeAjaxController.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Controller/SelectTreeController.php [deleted file]
typo3/sysext/backend/Classes/Controller/Wizard/SuggestWizardController.php
typo3/sysext/backend/Classes/Form/AbstractNode.php
typo3/sysext/backend/Classes/Form/Container/FlexFormContainerContainer.php
typo3/sysext/backend/Classes/Form/Container/FlexFormElementContainer.php
typo3/sysext/backend/Classes/Form/Container/FlexFormEntryContainer.php
typo3/sysext/backend/Classes/Form/Container/FlexFormNoTabsContainer.php
typo3/sysext/backend/Classes/Form/Container/FlexFormSectionContainer.php
typo3/sysext/backend/Classes/Form/Container/FlexFormTabsContainer.php
typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php
typo3/sysext/backend/Classes/Form/Container/InlineRecordContainer.php
typo3/sysext/backend/Classes/Form/Container/SingleFieldContainer.php
typo3/sysext/backend/Classes/Form/DatabaseFileIconsHookInterface.php
typo3/sysext/backend/Classes/Form/Element/AbstractFormElement.php
typo3/sysext/backend/Classes/Form/Element/GroupElement.php
typo3/sysext/backend/Classes/Form/Element/ImageManipulationElement.php
typo3/sysext/backend/Classes/Form/Element/InputColorPickerElement.php
typo3/sysext/backend/Classes/Form/Element/InputTextElement.php
typo3/sysext/backend/Classes/Form/Element/SelectCheckBoxElement.php
typo3/sysext/backend/Classes/Form/Element/SelectMultipleSideBySideElement.php
typo3/sysext/backend/Classes/Form/Element/SelectSingleBoxElement.php
typo3/sysext/backend/Classes/Form/Element/SelectSingleElement.php
typo3/sysext/backend/Classes/Form/Element/SelectTreeElement.php
typo3/sysext/backend/Classes/Form/FormDataCompiler.php
typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php
typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseRecordTypeValue.php
typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseRowDefaultValues.php
typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseUniqueUidNewRow.php
typo3/sysext/backend/Classes/Form/FormDataProvider/EvaluateDisplayConditions.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaFlexProcess.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaGroup.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInline.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInlineConfiguration.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInputPlaceholders.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaRecordTitle.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectItems.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectTreeItems.php
typo3/sysext/backend/Classes/Form/FormResultCompiler.php
typo3/sysext/backend/Classes/Form/Wizard/SuggestWizard.php
typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php
typo3/sysext/backend/Resources/Private/Templates/Wizards/SuggestWizard.html
typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SelectTreeElement.js
typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SvgTree.js
typo3/sysext/backend/Resources/Public/JavaScript/FormEngineFlexForm.js
typo3/sysext/backend/Resources/Public/JavaScript/FormEngineSuggest.js
typo3/sysext/backend/Tests/Unit/Controller/FormSelectTreeAjaxControllerTest.php [new file with mode: 0644]
typo3/sysext/backend/Tests/Unit/Controller/SelectTreeControllerTest.php [deleted file]
typo3/sysext/backend/Tests/Unit/Controller/Wizard/SuggestWizardControllerTest.php
typo3/sysext/backend/Tests/Unit/Form/Element/GroupElementTest.php [deleted file]
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseRecordTypeValueTest.php
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseUniqueUidNewRowTest.php
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/EvaluateDisplayConditionsTest.php
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaFlexProcessTest.php
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaGroupTest.php
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaInlineConfigurationTest.php
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaInlineTest.php
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaInputPlaceholdersTest.php
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaRecordTitleTest.php
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectItemsTest.php
typo3/sysext/backend/Tests/Unit/Form/NodeFactoryTest.php
typo3/sysext/backend/Tests/Unit/Form/Wizard/SuggestWizardTest.php
typo3/sysext/core/Classes/DataHandling/DataHandler.php
typo3/sysext/core/Classes/Database/RelationHandler.php
typo3/sysext/core/Classes/Migrations/TcaMigration.php
typo3/sysext/core/Classes/Resource/Service/UserFileInlineLabelService.php
typo3/sysext/core/Configuration/TCA/be_groups.php
typo3/sysext/core/Configuration/TCA/be_users.php
typo3/sysext/core/Configuration/TCA/pages.php
typo3/sysext/core/Configuration/TCA/sys_file_collection.php
typo3/sysext/core/Configuration/TCA/sys_file_storage.php
typo3/sysext/core/Configuration/TCA/sys_filemounts.php
typo3/sysext/core/Documentation/Changelog/master/Breaking-78899-DroppedFormEngineMethods.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Breaking-78899-RemovedExtJsCodeFromFormEngineResultArray.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Deprecation-78899-FormEngineMethods.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Deprecation-78899-TCACtrlFieldRequestUpdateDropped.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-78899-TCAMaxitemsOptional.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Important-78899-DisplayCondStrictParsing.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Migrations/TcaMigrationTest.php
typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapFactory.php
typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_blog.php
typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_person.php
typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php
typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_tag.php
typo3/sysext/frontend/Configuration/TCA/tt_content.php
typo3/sysext/lang/Resources/Private/Language/locallang_csh_corebe.xlf
typo3/sysext/rsaauth/Classes/Form/Element/RsaInputElement.php
typo3/sysext/sys_action/Configuration/TCA/sys_action.php
typo3/sysext/workspaces/Configuration/TCA/sys_workspace.php

diff --git a/typo3/sysext/backend/Classes/Controller/FormFlexAjaxController.php b/typo3/sysext/backend/Classes/Controller/FormFlexAjaxController.php
new file mode 100644 (file)
index 0000000..cbcf314
--- /dev/null
@@ -0,0 +1,163 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Backend\Controller;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Backend\Form\FormDataCompiler;
+use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
+use TYPO3\CMS\Backend\Form\NodeFactory;
+use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\StringUtility;
+
+/**
+ * Handle FormEngine flex field ajax calls
+ */
+class FormFlexAjaxController
+{
+    /**
+     * Render a single flex form section container to add it to the DOM
+     *
+     * @param ServerRequestInterface $request
+     * @param ResponseInterface $response
+     * @return ResponseInterface
+     */
+    public function containerAdd(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
+    {
+        $queryParameters = $request->getParsedBody();
+
+        $vanillaUid = (int)$queryParameters['vanillaUid'];
+        $databaseRowUid = $queryParameters['databaseRowUid'];
+        $command = $queryParameters['command'];
+        $tableName = $queryParameters['tableName'];
+        $fieldName = $queryParameters['fieldName'];
+        $recordTypeValue = $queryParameters['recordTypeValue'];
+        $dataStructureIdentifier = json_encode($queryParameters['dataStructureIdentifier']);
+        $flexFormSheetName = $queryParameters['flexFormSheetName'];
+        $flexFormFieldName = $queryParameters['flexFormFieldName'];
+        $flexFormContainerName = $queryParameters['flexFormContainerName'];
+
+        // Prepare TCA and data values for a new section container using data providers
+        $processedTca = $GLOBALS['TCA'][$tableName];
+        $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
+        $dataStructure = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
+        $processedTca['columns'][$fieldName]['config']['ds'] = $dataStructure;
+        $processedTca['columns'][$fieldName]['config']['dataStructureIdentifier'] = $dataStructureIdentifier;
+        // Get a new unique id for this container.
+        $flexFormContainerIdentifier = StringUtility::getUniqueId();
+        $flexSectionContainerPreparation = [
+            'flexFormSheetName' => $flexFormSheetName,
+            'flexFormFieldName' => $flexFormFieldName,
+            'flexFormContainerName' => $flexFormContainerName,
+            'flexFormContainerIdentifier' => $flexFormContainerIdentifier,
+        ];
+
+        $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
+        $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
+        $formDataCompilerInput = [
+            'tableName' => $tableName,
+            'vanillaUid' => (int)$vanillaUid,
+            'databaseRow' => [
+                'uid' => $databaseRowUid,
+            ],
+            'command' => $command,
+            'recordTypeValue' => $recordTypeValue,
+            'processedTca' => $processedTca,
+            'flexSectionContainerPreparation' => $flexSectionContainerPreparation,
+        ];
+        $formData = $formDataCompiler->compile($formDataCompilerInput);
+
+        $dataStructure = $formData['processedTca']['columns'][$fieldName]['config']['ds'];
+        $formData['fieldName'] = $fieldName;
+        $formData['flexFormDataStructureArray'] = $dataStructure['sheets'][$flexFormSheetName]['ROOT']['el'][$flexFormFieldName]['children'][$flexFormContainerIdentifier];
+        $formData['flexFormDataStructureIdentifier'] = $dataStructureIdentifier;
+        $formData['flexFormFieldName'] = $flexFormFieldName;
+        $formData['flexFormSheetName'] = $flexFormSheetName;
+        $formData['flexFormContainerName'] = $flexFormContainerName;
+        $formData['flexFormContainerIdentifier'] = $flexFormContainerIdentifier;
+        $formData['flexFormContainerElementCollapsed'] = false;
+
+        $formData['flexFormFormPrefix'] = '[data][' . $flexFormSheetName . '][lDEF]' . '[' . $flexFormFieldName . ']' . '[el]';
+        $formData['parameterArray']['itemFormElName'] = 'data[' . $tableName . '][' . $formData['databaseRow']['uid'] . '][' . $fieldName . ']';
+
+        // JavaScript code for event handlers:
+        // @todo: see if we can get rid of this - used in group elements, and also for the "reload" on type field changes
+        $formData['parameterArray']['fieldChangeFunc'] = [];
+        $formData['parameterArray']['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = 'TBE_EDITOR.fieldChanged('
+            . GeneralUtility::quoteJSvalue($tableName)
+            . ',' . GeneralUtility::quoteJSvalue($formData['databaseRow']['uid'])
+            . ',' . GeneralUtility::quoteJSvalue($fieldName)
+            . ',' . GeneralUtility::quoteJSvalue($formData['parameterArray']['itemFormElName'])
+            . ');';
+
+        // @todo: check GroupElement for usage of elementBaseName ... maybe kick that thing?
+
+        // Feed resulting form data to container structure to render HTML and other result data
+        $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
+        $formData['renderType'] = 'flexFormContainerContainer';
+        $newContainerResult = $nodeFactory->create($formData)->render();
+
+        $jsonResult = [
+            'html' => $newContainerResult['html'],
+            'scriptCall' => [],
+        ];
+
+        if (!empty($newContainerResult['additionalJavaScriptSubmit'])) {
+            $additionalJavaScriptSubmit = implode('', $newContainerResult['additionalJavaScriptSubmit']);
+            $additionalJavaScriptSubmit = str_replace([CR, LF], '', $additionalJavaScriptSubmit);
+            $jsonResult['scriptCall'][] = 'TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJavaScriptSubmit) . '");';
+        }
+        foreach ($newContainerResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) {
+            $jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost;
+        }
+        // @todo: handle stylesheetFiles, additionalInlineLanguageLabelFiles
+
+        // @todo: copied from inline ajax handler - maybe extract to some abstract?
+        if (!empty($newContainerResult['requireJsModules'])) {
+            foreach ($newContainerResult['requireJsModules'] as $module) {
+                $moduleName = null;
+                $callback = null;
+                if (is_string($module)) {
+                    // if $module is a string, no callback
+                    $moduleName = $module;
+                    $callback = null;
+                } elseif (is_array($module)) {
+                    // if $module is an array, callback is possible
+                    foreach ($module as $key => $value) {
+                        $moduleName = $key;
+                        $callback = $value;
+                        break;
+                    }
+                }
+                if ($moduleName !== null) {
+                    $inlineCodeKey = $moduleName;
+                    $javaScriptCode = 'require(["' . $moduleName . '"]';
+                    if ($callback !== null) {
+                        $inlineCodeKey .= sha1($callback);
+                        $javaScriptCode .= ', ' . $callback;
+                    }
+                    $javaScriptCode .= ');';
+                    $jsonResult['scriptCall'][] = '/*RequireJS-Module-' . $inlineCodeKey . '*/' . LF . $javaScriptCode;
+                }
+            }
+        }
+
+        $response->getBody()->write(json_encode($jsonResult));
+
+        return $response;
+    }
+}
index 6872350..e005f1c 100644 (file)
@@ -23,6 +23,7 @@ use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
 use TYPO3\CMS\Backend\Form\InlineStackProcessor;
 use TYPO3\CMS\Backend\Form\NodeFactory;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
 use TYPO3\CMS\Core\DataHandling\DataHandler;
 use TYPO3\CMS\Core\Localization\LocalizationFactory;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
@@ -75,18 +76,27 @@ class FormInlineAjaxController
             $databaseRow = [];
             $vanillaUid = (int)$inlineFirstPid;
         }
-        $databaseRow = $this->addFlexFormDataStructurePointersFromAjaxContext($ajaxArguments, $databaseRow);
+
+        $flexDataStructureIdentifier = $this->getFlexFormDataStructureIdentifierFromAjaxContext($ajaxArguments);
+        $processedTca = [];
+        if ($flexDataStructureIdentifier) {
+            $processedTca = $GLOBALS['TCA'][$parent['table']];
+            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
+            $dataStructure = $flexFormTools->parseDataStructureByIdentifier($flexDataStructureIdentifier);
+            $processedTca['columns'][$parentFieldName]['config']['dataStructureIdentifier'] = $flexDataStructureIdentifier;
+            $processedTca['columns'][$parentFieldName]['config']['ds'] = $dataStructure;
+        }
 
         $formDataCompilerInputForParent = [
             'vanillaUid' => $vanillaUid,
             'command' => $command,
             'tableName' => $parent['table'],
             'databaseRow' => $databaseRow,
+            'processedTca' => $processedTca,
             'inlineFirstPid' => $inlineFirstPid,
-            'columnsToProcess' => array_merge(
-                [$parentFieldName],
-                array_keys($databaseRow)
-            ),
+            'columnsToProcess' => [
+                $parentFieldName,
+            ],
             // Do not resolve existing children, we don't need them now
             'inlineResolveExistingChildren' => false,
         ];
@@ -248,18 +258,26 @@ class FormInlineAjaxController
             'uid' => (int)$parent['uid'],
         ];
 
-        $databaseRow = $this->addFlexFormDataStructurePointersFromAjaxContext($ajaxArguments, $databaseRow);
+        $flexDataStructureIdentifier = $this->getFlexFormDataStructureIdentifierFromAjaxContext($ajaxArguments);
+        $processedTca = [];
+        if ($flexDataStructureIdentifier) {
+            $processedTca = $GLOBALS['TCA'][$parent['table']];
+            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
+            $dataStructure = $flexFormTools->parseDataStructureByIdentifier($flexDataStructureIdentifier);
+            $processedTca['columns'][$parentFieldName]['config']['dataStructureIdentifier'] = $flexDataStructureIdentifier;
+            $processedTca['columns'][$parentFieldName]['config']['ds'] = $dataStructure;
+        }
 
         $formDataCompilerInputForParent = [
             'vanillaUid' => (int)$parent['uid'],
             'command' => 'edit',
             'tableName' => $parent['table'],
             'databaseRow' => $databaseRow,
+            'processedTca' => $processedTca,
             'inlineFirstPid' => $inlineFirstPid,
-            'columnsToProcess' => array_merge(
-                [$parentFieldName],
-                array_keys($databaseRow)
-            ),
+            'columnsToProcess' => [
+                $parentFieldName
+            ],
             // @todo: still needed?
             'inlineStructure' => $inlineStackProcessor->getStructure(),
             // Do not resolve existing children, we don't need them now
@@ -639,7 +657,6 @@ class FormInlineAjaxController
         foreach ($childResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) {
             $jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost;
         }
-        $jsonResult['scriptCall'][] = $childResult['extJSCODE'];
         if (!empty($childResult['additionalInlineLanguageLabelFiles'])) {
             $labels = [];
             foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) {
@@ -927,33 +944,25 @@ class FormInlineAjaxController
     }
 
     /**
-     * Flexforms require additional database columns to be processed to determine the correct
-     * data structure to be used from a flexform. The required columns and their values are
-     * transmitted in the AJAX context of the request and need to be added to the fake database
-     * row for the inline parent.
+     * Inline fields within a flex form need the data structure identifier that
+     * specifies the specific flex form this inline element is in. Retrieve it from
+     * the context array.
      *
      * @param array $ajaxArguments The AJAX request arguments
-     * @param array $databaseRow The fake database row
-     * @return array The database row with the flexform data structure pointer columns added
+     * @return string Data structure identifier as json string
      */
-    protected function addFlexFormDataStructurePointersFromAjaxContext(array $ajaxArguments, array $databaseRow)
+    protected function getFlexFormDataStructureIdentifierFromAjaxContext(array $ajaxArguments)
     {
         if (!isset($ajaxArguments['context'])) {
-            return $databaseRow;
+            return '';
         }
 
         $context = json_decode($ajaxArguments['context'], true);
         if (GeneralUtility::hmac(serialize($context['config'])) !== $context['hmac']) {
-            return $databaseRow;
-        }
-
-        if (isset($context['config']['flexDataStructurePointers'])
-            && is_array($context['config']['flexDataStructurePointers'])
-        ) {
-            $databaseRow = array_merge($context['config']['flexDataStructurePointers'], $databaseRow);
+            return '';
         }
 
-        return $databaseRow;
+        return $context['config']['flexDataStructureIdentifier'] ?? '';
     }
 
     /**
diff --git a/typo3/sysext/backend/Classes/Controller/FormSelectTreeAjaxController.php b/typo3/sysext/backend/Classes/Controller/FormSelectTreeAjaxController.php
new file mode 100644 (file)
index 0000000..913fcd2
--- /dev/null
@@ -0,0 +1,186 @@
+<?php
+namespace TYPO3\CMS\Backend\Controller;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Backend\Form\FormDataCompiler;
+use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
+use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Backend controller for selectTree ajax operations
+ */
+class FormSelectTreeAjaxController
+{
+    /**
+     * Returns json representing category tree
+     *
+     * @param ServerRequestInterface $request
+     * @param ResponseInterface $response
+     * @throws \RuntimeException
+     * @return ResponseInterface
+     */
+    public function fetchDataAction(ServerRequestInterface $request, ResponseInterface $response)
+    {
+        $tableName = $request->getQueryParams()['tableName'];
+        $fieldName = $request->getQueryParams()['fieldName'];
+
+        // Prepare processedTca: Remove all column definitions except the one that contains
+        // our tree definition. This way only this field is calculated, everything else is ignored.
+        if (!isset($GLOBALS['TCA'][$tableName])  || !is_array($GLOBALS['TCA'][$tableName])) {
+            throw new \RuntimeException(
+                'TCA for table ' . $tableName . ' not found',
+                1479386729
+            );
+        }
+        $processedTca = $GLOBALS['TCA'][$tableName];
+        if (!isset($processedTca['columns'][$fieldName]) || !is_array($processedTca['columns'][$fieldName])) {
+            throw new \RuntimeException(
+                'TCA for table ' . $tableName . ' and field ' . $fieldName . ' not found',
+                1479386990
+            );
+        }
+
+        // Force given record type and set showitem to our field only
+        $recordTypeValue = $request->getQueryParams()['recordTypeValue'];
+        $processedTca['types'][$recordTypeValue]['showitem'] = $fieldName;
+        // Unset all columns except our field
+        $processedTca['columns'] = [
+            $fieldName => $processedTca['columns'][$fieldName],
+        ];
+
+        $dataStructureIdentifier = '';
+        $flexFormSheetName = '';
+        $flexFormFieldName = '';
+        $flexFormContainerIdentifier = '';
+        $flexFormContainerFieldName = '';
+        $flexSectionContainerPreparation = [];
+        if ($processedTca['columns'][$fieldName]['config']['type'] === 'flex') {
+            if (!empty($request->getQueryParams()['dataStructureIdentifier'])) {
+                $dataStructureIdentifier = json_encode($request->getQueryParams()['dataStructureIdentifier']);
+            }
+            $flexFormSheetName = $request->getQueryParams()['flexFormSheetName'];
+            $flexFormFieldName = $request->getQueryParams()['flexFormFieldName'];
+            $flexFormContainerName = $request->getQueryParams()['flexFormContainerName'];
+            $flexFormContainerIdentifier = $request->getQueryParams()['flexFormContainerIdentifier'];
+            $flexFormContainerFieldName = $request->getQueryParams()['flexFormContainerFieldName'];
+            $flexFormSectionContainerIsNew = (bool)$request->getQueryParams()['flexFormSectionContainerIsNew'];
+
+            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
+            $dataStructure = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
+
+            // Reduce given data structure down to the relevant element only
+            if (empty($flexFormContainerFieldName)) {
+                if (isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
+                    ['el'][$flexFormFieldName])
+                ) {
+                    $dataStructure = [
+                        'sheets' => [
+                            $flexFormSheetName => [
+                                'ROOT' => [
+                                    'type' => 'array',
+                                    'el' => [
+                                        $flexFormFieldName => $dataStructure['sheets'][$flexFormSheetName]['ROOT']
+                                            ['el'][$flexFormFieldName],
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ];
+                }
+            } else {
+                if (isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
+                    ['el'][$flexFormFieldName]
+                    ['el'][$flexFormContainerName]
+                    ['el'][$flexFormContainerFieldName])
+                ) {
+                    // If this is a tree in a section container that has just been added by the FlexFormAjaxController
+                    // "new container" action, then this container is not yet persisted, so we need to trigger the
+                    // TcaFlexProcess data provider again to prepare the DS and databaseRow of that container.
+                    if ($flexFormSectionContainerIsNew) {
+                        $flexSectionContainerPreparation = [
+                            'flexFormSheetName' => $flexFormSheetName,
+                            'flexFormFieldName' => $flexFormFieldName,
+                            'flexFormContainerName' => $flexFormContainerName,
+                            'flexFormContainerIdentifier' => $flexFormContainerIdentifier,
+                        ];
+                    }
+                    // Now restrict the data structure to our tree element only
+                    $dataStructure = [
+                        'sheets' => [
+                            $flexFormSheetName => [
+                                'ROOT' => [
+                                    'type' => 'array',
+                                    'el' => [
+                                        $flexFormFieldName => [
+                                            'section' => 1,
+                                            'type' => 'array',
+                                            'el' => [
+                                                $flexFormContainerName => [
+                                                    'el' => [
+                                                        $flexFormContainerFieldName => $dataStructure['sheets'][$flexFormSheetName]['ROOT']
+                                                            ['el'][$flexFormFieldName]
+                                                            ['el'][$flexFormContainerName]
+                                                            ['el'][$flexFormContainerFieldName]
+                                                    ],
+                                                ],
+                                            ],
+                                        ],
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ];
+                }
+            }
+            $processedTca['columns'][$fieldName]['config']['ds'] = $dataStructure;
+            $processedTca['columns'][$fieldName]['config']['dataStructureIdentifier'] = $dataStructureIdentifier;
+        }
+
+        $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
+        $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
+        $formDataCompilerInput = [
+            'tableName' => $tableName,
+            'vanillaUid' => (int)$request->getQueryParams()['uid'],
+            'command' => $request->getQueryParams()['command'],
+            'processedTca' => $processedTca,
+            'recordTypeValue' => $recordTypeValue,
+            'selectTreeCompileItems' => true,
+            'flexSectionContainerPreparation' => $flexSectionContainerPreparation,
+        ];
+        $formData = $formDataCompiler->compile($formDataCompilerInput);
+
+        if ($formData['processedTca']['columns'][$fieldName]['config']['type'] === 'flex') {
+            if (empty($flexFormContainerFieldName)) {
+                $treeData = $formData['processedTca']['columns'][$fieldName]['config']['ds']
+                    ['sheets'][$flexFormSheetName]['ROOT']
+                    ['el'][$flexFormFieldName]['config']['items'];
+            } else {
+                $treeData = $formData['processedTca']['columns'][$fieldName]['config']['ds']
+                    ['sheets'][$flexFormSheetName]['ROOT']
+                    ['el'][$flexFormFieldName]
+                    ['children'][$flexFormContainerIdentifier]
+                    ['el'][$flexFormContainerFieldName]['config']['items'];
+            }
+        } else {
+            $treeData = $formData['processedTca']['columns'][$fieldName]['config']['items'];
+        }
+
+        $response->getBody()->write(json_encode($treeData));
+        return $response;
+    }
+}
diff --git a/typo3/sysext/backend/Classes/Controller/SelectTreeController.php b/typo3/sysext/backend/Classes/Controller/SelectTreeController.php
deleted file mode 100644 (file)
index eee0d93..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-<?php
-namespace TYPO3\CMS\Backend\Controller;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-use Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\ServerRequestInterface;
-use TYPO3\CMS\Backend\Form\FormDataCompiler;
-use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
-use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-
-/**
- * Backend controller for selectTree ajax operations
- */
-class SelectTreeController
-{
-    /**
-     * Returns json representing category tree
-     *
-     * @param ServerRequestInterface $request
-     * @param ResponseInterface $response
-     * @return ResponseInterface
-     */
-    public function fetchDataAction(ServerRequestInterface $request, ResponseInterface $response)
-    {
-        $tableName = $request->getQueryParams()['table'];
-        $fieldName = $request->getQueryParams()['field'];
-
-        // Prepare processedTca: Remove all column definitions except the one that contains
-        // our tree definition. This way only this field is calculated, everything else is ignored.
-        if (!isset($GLOBALS['TCA'][$tableName])  || !is_array($GLOBALS['TCA'][$tableName])) {
-            throw new \RuntimeException(
-                'TCA for table ' . $tableName . ' not found',
-                1479386729
-            );
-        }
-        $processedTca = $GLOBALS['TCA'][$tableName];
-        if (!isset($processedTca['columns'][$fieldName]) || !is_array($processedTca['columns'][$fieldName])) {
-            throw new \RuntimeException(
-                'TCA for table ' . $tableName . ' and field ' . $fieldName . ' not found',
-                1479386990
-            );
-        }
-
-        // Force given record type and set showitem to our field only
-        $recordTypeValue = $request->getQueryParams()['record_type_value'];
-        $processedTca['types'][$recordTypeValue]['showitem'] = $fieldName;
-        // Unset all columns except our field
-        $processedTca['columns'] = [
-            $fieldName => $processedTca['columns'][$fieldName],
-        ];
-
-        $flexFormPath = [];
-        if ($processedTca['columns'][$fieldName]['config']['type'] === 'flex') {
-            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
-            $dataStructureIdentifier = json_encode($request->getQueryParams()['flex_form_datastructure_identifier']);
-            $dataStructure = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
-            // Try to reduce given data structure down to the relevant element only
-            $flexFormPath = $request->getQueryParams()['flex_form_path'];
-            $fieldPattern = 'data[' . $tableName . '][';
-            $flexFormPath = str_replace($fieldPattern, '', $flexFormPath);
-            $flexFormPath = substr($flexFormPath, 0, -1);
-            $flexFormPath = explode('][', $flexFormPath);
-            if (isset($dataStructure['sheets'][$flexFormPath[3]]['ROOT']['el'][$flexFormPath[5]])) {
-                $dataStructure = [
-                    'sheets' => [
-                        $flexFormPath[3] => [
-                            'ROOT' => [
-                                'type' => 'array',
-                                'el' => [
-                                    $flexFormPath[5] => $dataStructure['sheets'][$flexFormPath[3]]['ROOT']['el'][$flexFormPath[5]],
-                                ],
-                            ],
-                        ],
-                    ],
-                ];
-            }
-            $processedTca['columns'][$fieldName]['config']['ds'] = $dataStructure;
-            $processedTca['columns'][$fieldName]['config']['dataStructureIdentifier'] = $dataStructureIdentifier;
-        }
-
-        $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
-        $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
-        $formDataCompilerInput = [
-            'tableName' => $request->getQueryParams()['table'],
-            'vanillaUid' => (int)$request->getQueryParams()['uid'],
-            'command' => $request->getQueryParams()['command'],
-            'processedTca' => $processedTca,
-            'recordTypeValue' => $recordTypeValue,
-            'selectTreeCompileItems' => true,
-        ];
-        $formData = $formDataCompiler->compile($formDataCompilerInput);
-
-        if ($formData['processedTca']['columns'][$fieldName]['config']['type'] === 'flex') {
-            $treeData = $formData['processedTca']['columns'][$fieldName]['config']['ds']
-                ['sheets'][$flexFormPath[3]]['ROOT']['el'][$flexFormPath[5]]['config']['items'];
-        } else {
-            $treeData = $formData['processedTca']['columns'][$fieldName]['config']['items'];
-        }
-
-        $json = json_encode($treeData);
-        $response->getBody()->write($json);
-        return $response;
-    }
-}
index 390ea7b..0047a31 100644 (file)
@@ -22,7 +22,6 @@ use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Core\Utility\MathUtility;
 
 /**
  * Receives ajax request from FormEngine suggest wizard and creates suggest answer as json result
@@ -41,75 +40,63 @@ class SuggestWizardController
     {
         $parsedBody = $request->getParsedBody();
 
-        if (!isset($parsedBody['value'])
-            || !isset($parsedBody['table'])
-            || !isset($parsedBody['field'])
-            || !isset($parsedBody['uid'])
-            || !isset($parsedBody['dataStructureIdentifier'])
-            || !isset($parsedBody['hmac'])
-        ) {
-            throw new \RuntimeException(
-                'Missing at least one of the required arguments "value", "table", "field", "uid"'
-                . ', "dataStructureIdentifier" or "hmac"',
-                1478607036
-            );
-        }
-
         $search = $parsedBody['value'];
-        $table = $parsedBody['table'];
-        $field = $parsedBody['field'];
+        $tableName = $parsedBody['tableName'];
+        $fieldName = $parsedBody['fieldName'];
         $uid = $parsedBody['uid'];
         $pid = (int)$parsedBody['pid'];
-
-        // flex form section container identifiers are created on js side dynamically "onClick". Those are
-        // not within the generated hmac ... the js side adds "idx{dateInMilliseconds}-", so this is removed here again.
-        // example outgoing in renderSuggestSelector():
-        // flex_1|data|sSuggestCheckCombination|lDEF|settings.subelements|el|ID-356586b0d3-form|item|el|content|vDEF
-        // incoming here:
-        // flex_1|data|sSuggestCheckCombination|lDEF|settings.subelements|el|ID-356586b0d3-idx1478611729574-form|item|el|content|vDEF
-        // Note: For existing containers, these parts are numeric, so "ID-356586b0d3-idx1478611729574-form" becomes 1 or 2, etc.
-        // @todo: This could be kicked is the flex form section containers are moved to an ajax call on creation
-        $fieldForHmac = preg_replace('/idx\d{13}-/', '', $field);
-
-        $dataStructureIdentifierString = '';
+        $dataStructureIdentifier = '';
         if (!empty($parsedBody['dataStructureIdentifier'])) {
-            $dataStructureIdentifierString = json_encode($parsedBody['dataStructureIdentifier']);
-        }
-
-        $incomingHmac = $parsedBody['hmac'];
-        $calculatedHmac = GeneralUtility::hmac(
-            $table . $fieldForHmac . $uid . $pid . $dataStructureIdentifierString,
-            'formEngineSuggest'
-        );
-        if ($incomingHmac !== $calculatedHmac) {
-            throw new \RuntimeException(
-                'Incoming and calculated hmac do not match',
-                1478608245
-            );
-        }
-
-        // If the $uid is numeric (existing page) and a suggest wizard in pages is handled, the effective
-        // pid is the uid of that page - important for page ts config configuration.
-        if (MathUtility::canBeInterpretedAsInteger($uid) && $table === 'pages') {
-            $pid = $uid;
+            $dataStructureIdentifier = json_encode($parsedBody['dataStructureIdentifier']);
         }
-        $TSconfig = BackendUtility::getPagesTSconfig($pid);
+        $flexFormSheetName = $parsedBody['flexFormSheetName'];
+        $flexFormFieldName = $parsedBody['flexFormFieldName'];
+        $flexFormContainerName = $parsedBody['flexFormContainerName'];
+        $flexFormContainerFieldName = $parsedBody['flexFormContainerFieldName'];
 
         // Determine TCA config of field
-        if (empty($dataStructureIdentifierString)) {
+        if (empty($dataStructureIdentifier)) {
             // Normal columns field
-            $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
+            $fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
+            $fieldNameInPageTsConfig = $fieldName;
         } else {
             // A flex flex form field
             $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
-            $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifierString);
-            $parts = explode('|', $field);
-            $fieldConfig = $this->getFlexFieldConfiguration($parts, $dataStructureArray);
-            // Flexform field name levels are separated with | instead of encapsulation in [];
-            // reverse this here to be compatible with regular field names.
-            $field = str_replace('|', '][', $field);
+            $dataStructure = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
+            if (empty($flexFormContainerFieldName)) {
+                // @todo: See if a path in pageTsConfig like "TCEForm.tableName.theContainerFieldName =" is useful and works with other pageTs, too.
+                $fieldNameInPageTsConfig = $flexFormFieldName;
+                if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
+                    ['el'][$flexFormFieldName]['TCEforms']['config'])
+                ) {
+                    throw new \RuntimeException(
+                        'Specified path ' . $flexFormFieldName . ' not found in flex form data structure',
+                        1480609491
+                    );
+                }
+                $fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT']
+                    ['el'][$flexFormFieldName]['TCEforms']['config'];
+            } else {
+                $fieldNameInPageTsConfig = $flexFormContainerFieldName;
+                if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
+                        ['el'][$flexFormFieldName]
+                        ['el'][$flexFormContainerName]
+                        ['el'][$flexFormContainerFieldName]['TCEforms']['config'])
+                ) {
+                    throw new \RuntimeException(
+                        'Specified path ' . $flexFormContainerName . ' not found in flex form section container data structure',
+                        1480611208
+                    );
+                }
+                $fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT']
+                    ['el'][$flexFormFieldName]
+                    ['el'][$flexFormContainerName]
+                    ['el'][$flexFormContainerFieldName]['TCEforms']['config'];
+            }
         }
 
+        $pageTsConfig = BackendUtility::getPagesTSconfig($pid);
+
         $wizardConfig = $fieldConfig['wizards']['suggest'];
 
         $queryTables = $this->getTablesToQueryFromFieldConfiguration($fieldConfig);
@@ -125,7 +112,7 @@ class SuggestWizardController
                 continue;
             }
 
-            $config = $this->getConfigurationForTable($queryTable, $wizardConfig, $TSconfig, $table, $field);
+            $config = $this->getConfigurationForTable($queryTable, $wizardConfig, $pageTsConfig, $tableName, $fieldNameInPageTsConfig);
 
             // process addWhere
             if (!isset($config['addWhere']) && $whereClause) {
@@ -136,8 +123,8 @@ class SuggestWizardController
                     '###THIS_UID###' => (int)$uid,
                     '###CURRENT_PID###' => (int)$pid
                 ];
-                if (isset($TSconfig['TCEFORM.'][$table . '.'][$field . '.'])) {
-                    $fieldTSconfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.'];
+                if (isset($pageTsConfig['TCEFORM.'][$tableName . '.'][$fieldNameInPageTsConfig . '.'])) {
+                    $fieldTSconfig = $pageTsConfig['TCEFORM.'][$tableName . '.'][$fieldNameInPageTsConfig . '.'];
                     if (isset($fieldTSconfig['PAGE_TSCONFIG_ID'])) {
                         $replacement['###PAGE_TSCONFIG_ID###'] = (int)$fieldTSconfig['PAGE_TSCONFIG_ID'];
                     }
@@ -213,44 +200,6 @@ class SuggestWizardController
     }
 
     /**
-     * Get 'config' section of field from resolved data structure specified by flex form path in $parts
-     *
-     * @param array $parts
-     * @param array $dataStructure
-     * @return array
-     */
-    protected function getFlexFieldConfiguration(array $parts, array $dataStructure)
-    {
-        if (count($parts) === 6) {
-            // Search a flex field, example:
-            // flex_1|data|sDb|lDEF|group_db_1|vDEF
-            if (!isset($dataStructure['sheets'][$parts[2]]['ROOT']['el'][$parts[4]]['TCEforms']['config'])) {
-                throw new \RuntimeException(
-                    'Specified path ' . implode('|', $parts) . ' not found in flex form data structure',
-                    1480609491
-                );
-            }
-            $fieldConfig = $dataStructure['sheets'][$parts[2]]['ROOT']['el'][$parts[4]]['TCEforms']['config'];
-        } elseif (count($parts) === 11) {
-            // Search a flex field in a section container, example:
-            // flex_1|data|sSuggestCheckCombination|lDEF|settings.subelements|el|1|item|el|content|vDEF
-            if (!isset($dataStructure['sheets'][$parts[2]]['ROOT']['el'][$parts[4]]['el'][$parts[7]]['el'][$parts[9]]['TCEforms']['config'])) {
-                throw new \RuntimeException(
-                    'Specified path ' . implode('|', $parts) . ' not found in flex form section container data structure',
-                    1480611208
-                );
-            }
-            $fieldConfig = $dataStructure['sheets'][$parts[2]]['ROOT']['el'][$parts[4]]['el'][$parts[7]]['el'][$parts[9]]['TCEforms']['config'];
-        } else {
-            throw new \RuntimeException(
-                'Invalid flex form path ' . implode('|', $parts),
-                1480611252
-            );
-        }
-        return $fieldConfig;
-    }
-
-    /**
      * Returns the configuration for the suggest wizard for the given table. This does multiple overlays from the
      * TSconfig.
      *
index 0981adb..124f653 100644 (file)
@@ -70,7 +70,6 @@ abstract class AbstractNode implements NodeInterface
             'stylesheetFiles' => [],
             // can hold strings or arrays, string = requireJS module, array = requireJS module + callback e.g. array('TYPO3/Foo/Bar', 'function() {}')
             'requireJsModules' => [],
-            'extJSCODE' => '',
             'inlineData' => [],
             'html' => '',
         ];
@@ -161,21 +160,13 @@ abstract class AbstractNode implements NodeInterface
         }
         if (!empty($config['maxitems']) || !empty($config['minitems'])) {
             $minItems = (isset($config['minitems'])) ? (int)$config['minitems'] : 0;
-            $maxItems = (isset($config['maxitems'])) ? (int)$config['maxitems'] : 10000;
+            $maxItems = (isset($config['maxitems'])) ? (int)$config['maxitems'] : 99999;
             $type = ($config['type']) ?: 'range';
-            if ($config['type'] === 'select' && $config['renderType'] !== 'selectTree' && $maxItems <= 1 && $minItems > 0) {
-                $validationRules[] = [
-                    'type' => $type,
-                    'minItems' => 1,
-                    'maxItems' => 100000
-                ];
-            } else {
-                $validationRules[] = [
-                    'type' => $type,
-                    'minItems' => $minItems,
-                    'maxItems' => $maxItems
-                ];
-            }
+            $validationRules[] = [
+                'type' => $type,
+                'minItems' => $minItems,
+                'maxItems' => $maxItems
+            ];
         }
         if (!empty($config['required'])) {
             $validationRules[] = ['type' => 'required'];
index e8a1d77..7b2f04b 100644 (file)
@@ -34,18 +34,16 @@ class FlexFormContainerContainer extends AbstractContainer
      */
     public function render()
     {
+        $languageService = $this->getLanguageService();
+
         $table = $this->data['tableName'];
         $row = $this->data['databaseRow'];
         $fieldName = $this->data['fieldName'];
         $flexFormFormPrefix = $this->data['flexFormFormPrefix'];
         $flexFormContainerElementCollapsed = $this->data['flexFormContainerElementCollapsed'];
-        $flexFormContainerTitle = $this->data['flexFormContainerTitle'];
-        $flexFormFieldIdentifierPrefix = $this->data['flexFormFieldIdentifierPrefix'];
+        $flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
         $parameterArray = $this->data['parameterArray'];
 
-        // Every container adds its own part to the id prefix
-        $flexFormFieldIdentifierPrefix = $flexFormFieldIdentifierPrefix . '-' . GeneralUtility::shortMD5(uniqid('id', true));
-
         $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
         $toggleIcons = '<span class="t3js-flex-control-toggle-icon-open" style="' . ($flexFormContainerElementCollapsed ? 'display: none;' : '') . '">'
             . $iconFactory->getIcon('actions-view-list-collapse', Icon::SIZE_SMALL)->render()
@@ -54,33 +52,38 @@ class FlexFormContainerContainer extends AbstractContainer
             . $iconFactory->getIcon('actions-view-list-expand', Icon::SIZE_SMALL)->render()
             . '</span>';
 
-        $flexFormContainerCounter = $this->data['flexFormContainerCounter'];
+        $flexFormContainerIdentifier = $this->data['flexFormContainerIdentifier'];
         $actionFieldName = '_ACTION_FLEX_FORM'
             . $parameterArray['itemFormElName']
             . $this->data['flexFormFormPrefix']
             . '[_ACTION]'
-            . '[' . $flexFormContainerCounter . ']';
+            . '[' . $flexFormContainerIdentifier . ']';
         $toggleFieldName = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']'
             . $flexFormFormPrefix
-            . '[' . $flexFormContainerCounter . ']'
+            . '[' . $flexFormContainerIdentifier . ']'
             . '[_TOGGLE]';
 
         $moveAndDeleteContent = [];
         $userHasAccessToDefaultLanguage = $this->getBackendUserAuthentication()->checkLanguageAccess(0);
         if ($userHasAccessToDefaultLanguage) {
-            $moveAndDeleteContent[] = '<span class="btn btn-default t3js-sortable-handle"><span title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:sortable.dragmove')) . '">' . $iconFactory->getIcon('actions-move-move', Icon::SIZE_SMALL)->render() . '</span></span>';
-            $moveAndDeleteContent[] = '<span class="btn btn-default t3js-delete"><span title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:delete')) . '">' . $iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</span></span>';
+            $moveAndDeleteContent[] = '<span class="btn btn-default t3js-sortable-handle"><span title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:sortable.dragmove')) . '">' . $iconFactory->getIcon('actions-move-move', Icon::SIZE_SMALL)->render() . '</span></span>';
+            $moveAndDeleteContent[] = '<span class="btn btn-default t3js-delete"><span title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:delete')) . '">' . $iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</span></span>';
         }
 
         $options = $this->data;
-        $options['flexFormFieldIdentifierPrefix'] = $flexFormFieldIdentifierPrefix;
         // Append container specific stuff to field prefix
-        $options['flexFormFormPrefix'] =  $flexFormFormPrefix . '[' . $flexFormContainerCounter . '][' . $this->data['flexFormContainerName'] . '][el]';
+        $options['flexFormFormPrefix'] =  $flexFormFormPrefix . '[' . $flexFormContainerIdentifier . '][' . $this->data['flexFormContainerName'] . '][el]';
+        $options['flexFormDataStructureArray'] = $flexFormDataStructureArray['el'];
         $options['renderType'] = 'flexFormElementContainer';
         $containerContentResult = $this->nodeFactory->create($options)->render();
 
+        $containerTitle = '';
+        if (!empty(trim($flexFormDataStructureArray['title']))) {
+            $containerTitle = $languageService->sL(trim($flexFormDataStructureArray['title']));
+        }
+
         $html = [];
-        $html[] = '<div id="' . $flexFormFieldIdentifierPrefix . '" class="t3-form-field-container-flexsections t3-flex-section t3js-flex-section">';
+        $html[] = '<div class="t3-form-field-container-flexsections t3-flex-section t3js-flex-section">';
         $html[] =    '<input class="t3-flex-control t3js-flex-control-action" type="hidden" name="' . htmlspecialchars($actionFieldName) . '" value="" />';
         $html[] =    '<div class="panel panel-default panel-condensed">';
         $html[] =        '<div class="panel-heading t3js-flex-section-header" data-toggle="formengine-flex">';
@@ -89,7 +92,7 @@ class FlexFormContainerContainer extends AbstractContainer
         $html[] =                    $toggleIcons;
         $html[] =                '</div>';
         $html[] =                '<div class="form-irre-header-cell form-irre-header-body">';
-        $html[] =                    '<span class="t3js-record-title">' . $flexFormContainerTitle . '</span>';
+        $html[] =                    '<span class="t3js-record-title">' . htmlspecialchars($containerTitle) . '</span>';
         $html[] =                '</div>';
         $html[] =                '<div class="form-irre-header-cell form-irre-header-control">';
         $html[] =                    '<div class="btn-group btn-group-sm">';
@@ -103,7 +106,6 @@ class FlexFormContainerContainer extends AbstractContainer
         $html[] =        '</div>';
         $html[] =        '<input';
         $html[] =            'class="t3-flex-control t3js-flex-control-toggle"';
-        $html[] =            'id="' . $flexFormFieldIdentifierPrefix . '-toggleClosed"';
         $html[] =            'type="hidden"';
         $html[] =            'name="' . htmlspecialchars($toggleFieldName) . '"';
         $html[] =            'value="' . ($flexFormContainerElementCollapsed ? '1' : '0') . '"';
index c8b7cc8..774fed1 100644 (file)
@@ -17,7 +17,6 @@ namespace TYPO3\CMS\Backend\Form\Container;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Lang\LanguageService;
 
 /**
@@ -25,7 +24,7 @@ use TYPO3\CMS\Lang\LanguageService;
  *
  * This one is called by FlexFormTabsContainer, FlexFormNoTabsContainer or FlexFormContainerContainer.
  * For single fields, the code is similar to SingleFieldContainer, processing will end up in single
- * element classes depending on specific type of an element. Additionally, it determines if a
+ * element classes depending on specific renderType of an element. Additionally, it determines if a
  * section is handled and hands over to FlexFormSectionContainer in this case.
  */
 class FlexFormElementContainer extends AbstractContainer
@@ -62,16 +61,10 @@ class FlexFormElementContainer extends AbstractContainer
                     continue;
                 }
 
-                $sectionTitle = '';
-                if (!empty(trim($flexFormFieldArray['title']))) {
-                    $sectionTitle = $languageService->sL(trim($flexFormFieldArray['title']));
-                }
-
                 $options = $this->data;
-                $options['flexFormDataStructureArray'] = $flexFormFieldArray['el'];
+                $options['flexFormDataStructureArray'] = $flexFormFieldArray;
                 $options['flexFormRowData'] = isset($flexFormRowData[$flexFormFieldName]['el']) ? $flexFormRowData[$flexFormFieldName]['el'] : [];
-                $options['flexFormSectionType'] = $flexFormFieldName;
-                $options['flexFormSectionTitle'] = $sectionTitle;
+                $options['flexFormFieldName'] = $flexFormFieldName;
                 $options['renderType'] = 'flexFormSectionContainer';
                 $sectionContainerResult = $this->nodeFactory->create($options)->render();
                 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $sectionContainerResult);
@@ -90,13 +83,16 @@ class FlexFormElementContainer extends AbstractContainer
                 ];
 
                 $alertMsgOnChange = '';
-                if (
-                    $fakeParameterArray['fieldConf']['onChange'] === 'reload'
-                    || !empty($GLOBALS['TCA'][$table]['ctrl']['type']) && $GLOBALS['TCA'][$table]['ctrl']['type'] === $flexFormFieldName
-                    || !empty($GLOBALS['TCA'][$table]['ctrl']['requestUpdate']) && GeneralUtility::inList($GLOBALS['TCA'][$table]['ctrl']['requestUpdate'], $flexFormFieldName)
-                ) {
+                if (isset($fakeParameterArray['fieldConf']['onChange']) && $fakeParameterArray['fieldConf']['onChange'] === 'reload') {
                     if ($this->getBackendUserAuthentication()->jsConfirmation(JsConfirmation::TYPE_CHANGE)) {
-                        $alertMsgOnChange = 'top.TYPO3.Modal.confirm(TYPO3.lang["FormEngine.refreshRequiredTitle"], TYPO3.lang["FormEngine.refreshRequiredContent"]).on("button.clicked", function(e) { if (e.target.name == "ok" && TBE_EDITOR.checkSubmit(-1)) { TBE_EDITOR.submitForm() } top.TYPO3.Modal.dismiss(); });';
+                        $alertMsgOnChange = 'top.TYPO3.Modal.confirm('
+                                . 'TYPO3.lang["FormEngine.refreshRequiredTitle"],'
+                                . ' TYPO3.lang["FormEngine.refreshRequiredContent"]'
+                            . ')'
+                            . '.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();}';
                     }
@@ -115,6 +111,7 @@ class FlexFormElementContainer extends AbstractContainer
                         $fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = str_replace($originalFieldName, $fakeParameterArray['itemFormElName'], $fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged']);
                     }
                 }
+                // @todo: is that a bug? name and id should usually be of different form
                 $fakeParameterArray['itemFormElID'] = $fakeParameterArray['itemFormElName'];
                 if (isset($flexFormRowData[$flexFormFieldName]['vDEF'])) {
                     $fakeParameterArray['itemFormElValue'] = $flexFormRowData[$flexFormFieldName]['vDEF'];
@@ -123,6 +120,12 @@ class FlexFormElementContainer extends AbstractContainer
                 }
 
                 $options = $this->data;
+                // Set either flexFormFieldName or flexFormContainerFieldName, depending on if we are a "regular" field or a flex container section field
+                if (empty($options['flexFormFieldName'])) {
+                    $options['flexFormFieldName'] = $flexFormFieldName;
+                } else {
+                    $options['flexFormContainerFieldName'] = $flexFormFieldName;
+                }
                 $options['parameterArray'] = $fakeParameterArray;
                 $options['elementBaseName'] = $this->data['elementBaseName'] . $flexFormFormPrefix . '[' . $flexFormFieldName . '][vDEF]';
 
index e6480c6..839a340 100644 (file)
@@ -29,9 +29,11 @@ class FlexFormEntryContainer extends AbstractContainer
      */
     public function render()
     {
+        $flexFormDataStructureIdentifier = $this->data['parameterArray']['fieldConf']['config']['dataStructureIdentifier'];
         $flexFormDataStructureArray = $this->data['parameterArray']['fieldConf']['config']['ds'];
 
         $options = $this->data;
+        $options['flexFormDataStructureIdentifier'] = $flexFormDataStructureIdentifier;
         $options['flexFormDataStructureArray'] = $flexFormDataStructureArray;
         $options['flexFormRowData'] = $this->data['parameterArray']['itemFormElValue'];
         $options['renderType'] = 'flexFormNoTabsContainer';
index 98c190e..7d3c021 100644 (file)
@@ -40,7 +40,6 @@ class FlexFormNoTabsContainer extends AbstractContainer
         $flexFormRowData = $this->data['flexFormRowData'];
         $resultArray = $this->initializeResultArray();
 
-        // Flex ds was normalized in flex provider to always have a sheet.
         // Determine this single sheet name, most often it ends up with sDEF, except if only one sheet was defined
         $sheetName = array_pop(array_keys($flexFormDataStructureArray['sheets']));
         $flexFormRowDataSubPart = $flexFormRowData['data'][$sheetName]['lDEF'] ?: [];
@@ -68,6 +67,7 @@ class FlexFormNoTabsContainer extends AbstractContainer
         $options = $this->data;
         $options['flexFormDataStructureArray'] = $flexFormDataStructureArray['sheets'][$sheetName]['ROOT']['el'];
         $options['flexFormRowData'] = $flexFormRowDataSubPart;
+        $options['flexFormSheetName'] = $sheetName;
         $options['flexFormFormPrefix'] = '[data][' . $sheetName . '][lDEF]';
         $options['parameterArray'] = $parameterArray;
 
index 02d6eff..b263246 100644 (file)
@@ -18,16 +18,16 @@ use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Imaging\IconFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Core\Utility\StringUtility;
 use TYPO3\CMS\Lang\LanguageService;
 
 /**
  * Handle flex form sections.
  *
  * This container is created by FlexFormElementContainer if a "single" element is in
- * fact a sections. For each existing section container it creates as FlexFormContainerContainer
- * to render its inner fields, additionally for each possible container a "template" of this
- * container type is rendered and added - to be added by JS to DOM on click on "new xy container".
+ * fact a section. For each existing section container it creates as FlexFormContainerContainer
+ * to render its inner fields.
+ * Additionally, a button for each possible container is rendered with information for the
+ * ajax controller that fetches one on click.
  */
 class FlexFormSectionContainer extends AbstractContainer
 {
@@ -38,133 +38,80 @@ class FlexFormSectionContainer extends AbstractContainer
      */
     public function render()
     {
-        /** @var IconFactory $iconFactory */
         $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
         $languageService = $this->getLanguageService();
 
-        $flexFormFieldsArray = $this->data['flexFormDataStructureArray'];
+        $flexFormDataStructureIdentifier = $this->data['flexFormDataStructureIdentifier'];
+        $flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
         $flexFormRowData = $this->data['flexFormRowData'];
-        $flexFormFieldIdentifierPrefix = $this->data['flexFormFieldIdentifierPrefix'];
-        $flexFormSectionType = $this->data['flexFormSectionType'];
-        $flexFormSectionTitle = $this->data['flexFormSectionTitle'];
+        $flexFormFieldName = $this->data['flexFormFieldName'];
+        $flexFormSheetName = $this->data['flexFormSheetName'];
 
         $userHasAccessToDefaultLanguage = $this->getBackendUserAuthentication()->checkLanguageAccess(0);
 
         $resultArray = $this->initializeResultArray();
 
-        // Creating IDs for form fields:
-        // It's important that the IDs "cascade" - otherwise we can't dynamically expand the flex form
-        // because this relies on simple string substitution of the first parts of the id values.
-        $flexFormFieldIdentifierPrefix = $flexFormFieldIdentifierPrefix . '-' . GeneralUtility::shortMD5(uniqid('id', true));
-
         // Render each existing container
-        foreach ($flexFormRowData as $flexFormContainerCounter => $existingSectionContainerData) {
+        foreach ($flexFormDataStructureArray['children'] as $flexFormContainerIdentifier => $containerDataStructure) {
+            $existingContainerData = $flexFormRowData[$flexFormContainerIdentifier];
             // @todo: This relies on the fact that "_TOGGLE" is *below* the real data in the saved xml structure
-            if (is_array($existingSectionContainerData)) {
-                $existingSectionContainerDataStructureType = key($existingSectionContainerData);
-                $existingSectionContainerData = $existingSectionContainerData[$existingSectionContainerDataStructureType];
-                $containerDataStructure = $flexFormFieldsArray[$existingSectionContainerDataStructureType];
-                // There may be cases where a field is still in DB but does not exist in definition
-                if (is_array($containerDataStructure)) {
-                    $sectionTitle = '';
-                    if (!empty(trim($containerDataStructure['title']))) {
-                        $sectionTitle = $languageService->sL(trim($containerDataStructure['title']));
-                    }
-
-                    $options = $this->data;
-                    $options['flexFormRowData'] = $existingSectionContainerData['el'];
-                    $options['flexFormDataStructureArray'] = $containerDataStructure['el'];
-                    $options['flexFormFieldIdentifierPrefix'] = $flexFormFieldIdentifierPrefix;
-                    $options['flexFormFormPrefix'] = $this->data['flexFormFormPrefix'] . '[' . $flexFormSectionType . ']' . '[el]';
-                    $options['flexFormContainerName'] = $existingSectionContainerDataStructureType;
-                    $options['flexFormContainerCounter'] = $flexFormContainerCounter;
-                    $options['flexFormContainerTitle'] = $sectionTitle;
-                    $options['flexFormContainerElementCollapsed'] = (bool)$existingSectionContainerData['el']['_TOGGLE'];
-                    $options['renderType'] = 'flexFormContainerContainer';
-                    $flexFormContainerContainerResult = $this->nodeFactory->create($options)->render();
-                    $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $flexFormContainerContainerResult);
-                }
-            }
+            $existingSectionContainerDataStructureType = key($existingContainerData);
+            $existingContainerData = $existingContainerData[$existingSectionContainerDataStructureType];
+            $options = $this->data;
+            $options['flexFormRowData'] = $existingContainerData['el'];
+            $options['flexFormDataStructureArray'] = $containerDataStructure;
+            $options['flexFormFormPrefix'] = $this->data['flexFormFormPrefix'] . '[' . $flexFormFieldName . ']' . '[el]';
+            $options['flexFormContainerName'] = $existingSectionContainerDataStructureType;
+            $options['flexFormContainerIdentifier'] = $flexFormContainerIdentifier;
+            $options['flexFormContainerElementCollapsed'] = (bool)$existingContainerData['el']['_TOGGLE'];
+            $options['renderType'] = 'flexFormContainerContainer';
+            $flexFormContainerContainerResult = $this->nodeFactory->create($options)->render();
+            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $flexFormContainerContainerResult);
         }
 
-        // "New container" handling: Creates a "template" of each possible container and stuffs it
-        // somewhere into DOM to be handled with JS magic.
-        // Fun part: Handle the fact that such things may be set for children
+        // "New container" handling: Creates buttons for each possible container with all relevant information for the ajax call.
         $containerTemplatesHtml = [];
-        foreach ($flexFormFieldsArray as $flexFormContainerName => $flexFormFieldDefinition) {
-            $containerTemplateHtml = [];
-            $sectionTitle = '';
+        foreach ($flexFormDataStructureArray['el'] as $flexFormContainerName => $flexFormFieldDefinition) {
+            $containerTitle = '';
             if (!empty(trim($flexFormFieldDefinition['title']))) {
-                $sectionTitle = $languageService->sL(trim($flexFormFieldDefinition['title']));
+                $containerTitle = $languageService->sL(trim($flexFormFieldDefinition['title']));
             }
-
-            $options = $this->data;
-            // @todo: this should use the prepared templateRow parallel to the single elements to have support of default values!
-            $options['flexFormRowData'] = [];
-            $options['flexFormDataStructureArray'] = $flexFormFieldDefinition['el'];
-            $options['flexFormFieldIdentifierPrefix'] = $flexFormFieldIdentifierPrefix;
-            $options['flexFormFormPrefix'] = $this->data['flexFormFormPrefix'] . '[' . $flexFormSectionType . ']' . '[el]';
-            $options['flexFormContainerName'] = $flexFormContainerName;
-            $options['flexFormContainerCounter'] = $flexFormFieldIdentifierPrefix . '-form';
-            $options['flexFormContainerTitle'] = $sectionTitle;
-            $options['flexFormContainerElementCollapsed'] = false;
-            $options['renderType'] = 'flexFormContainerContainer';
-            $flexFormContainerContainerTemplateResult = $this->nodeFactory->create($options)->render();
-
-            // Extract the random identifier used by the ExtJS tree. This is used later on in the onClick handler
-            // to dynamically modify the javascript code and instanciate a unique ExtJS tree instance per section.
-            $treeElementIdentifier = '';
-            if (!empty($flexFormContainerContainerTemplateResult['extJSCODE'])) {
-                if (preg_match('/StandardTreeItemData\["([a-f0-9]{32})"\]/', $flexFormContainerContainerTemplateResult['extJSCODE'], $matches)) {
-                    $treeElementIdentifier = $matches[1];
-                }
-            }
-
-            $uniqueId = StringUtility::getUniqueId('idvar');
-            $identifierPrefixJs = 'replace(/' . $flexFormFieldIdentifierPrefix . '-/g,"' . $flexFormFieldIdentifierPrefix . '-"+' . $uniqueId . '+"-")';
-            $identifierPrefixJs .= '.replace(/(tceforms-(datetime|date)field-)/g,"$1" + (new Date()).getTime())';
-
-            if (!empty($treeElementIdentifier)) {
-                $identifierPrefixJs .= '.replace(/(tree_?)?' . $treeElementIdentifier . '/g,"$1" + (' . $uniqueId . '))';
-            }
-
-            $onClickInsert = [];
-            $onClickInsert[] = 'var ' . $uniqueId . ' = "' . 'idx"+(new Date()).getTime();';
-            $onClickInsert[] = 'TYPO3.jQuery("#' . $flexFormFieldIdentifierPrefix . '").append(TYPO3.jQuery(' . json_encode($flexFormContainerContainerTemplateResult['html']) . '.' . $identifierPrefixJs . '));';
-            $onClickInsert[] = 'TYPO3.jQuery("#' . $flexFormFieldIdentifierPrefix . '").t3FormEngineFlexFormElement();';
-            $onClickInsert[] = 'eval(unescape("' . rawurlencode(implode(';', $flexFormContainerContainerTemplateResult['additionalJavaScriptPost'])) . '").' . $identifierPrefixJs . ');';
-            if (!empty($treeElementIdentifier)) {
-                $onClickInsert[] = 'eval(unescape("' . rawurlencode($flexFormContainerContainerTemplateResult['extJSCODE']) . '").' . $identifierPrefixJs . ');';
-            }
-            $onClickInsert[] = 'TBE_EDITOR.addActionChecks("submit", unescape("' . rawurlencode(implode(';', $flexFormContainerContainerTemplateResult['additionalJavaScriptSubmit'])) . '").' . $identifierPrefixJs . ');';
-            $onClickInsert[] = 'TYPO3.FormEngine.reinitialize();';
-            $onClickInsert[] = 'TYPO3.FormEngine.Validation.initializeInputFields();';
-            $onClickInsert[] = 'TYPO3.FormEngine.Validation.validate();';
-            $onClickInsert[] = 'return false;';
-
-            $containerTemplateHtml[] = '<a href="#" class="btn btn-default" onclick="' . htmlspecialchars(implode(LF, $onClickInsert)) . '">';
+            $containerTemplateHtml = [];
+            $containerTemplateHtml[] = '<a';
+            $containerTemplateHtml[] =     'href="#"';
+            $containerTemplateHtml[] =     'class="btn btn-default t3js-flex-container-add"';
+            $containerTemplateHtml[] =     'data-vanillauid="' . (int)$this->data['vanillaUid'] . '"';
+            // no int cast for databaseRow uid, this can be "NEW1234..."
+            $containerTemplateHtml[] =     'data-databaserowuid="' . htmlspecialchars($this->data['databaseRow']['uid']) . '"';
+            $containerTemplateHtml[] =     'data-command="' . htmlspecialchars($this->data['command']) . '"';
+            $containerTemplateHtml[] =     'data-tablename="' . htmlspecialchars($this->data['tableName']) . '"';
+            $containerTemplateHtml[] =     'data-fieldname="' . htmlspecialchars($this->data['fieldName']) . '"';
+            $containerTemplateHtml[] =     'data-recordtypevalue="' . $this->data['recordTypeValue'] . '"';
+            $containerTemplateHtml[] =     'data-datastructureidentifier="' . htmlspecialchars($flexFormDataStructureIdentifier) . '"';
+            $containerTemplateHtml[] =     'data-flexformsheetname="' . htmlspecialchars($flexFormSheetName) . '"';
+            $containerTemplateHtml[] =     'data-flexformfieldname="' . htmlspecialchars($flexFormFieldName) . '"';
+            $containerTemplateHtml[] =     'data-flexformcontainername="' . htmlspecialchars($flexFormContainerName) . '"';
+            $containerTemplateHtml[] = '>';
             $containerTemplateHtml[] =    $iconFactory->getIcon('actions-document-new', Icon::SIZE_SMALL)->render();
-            $containerTemplateHtml[] =    htmlspecialchars(GeneralUtility::fixed_lgd_cs($sectionTitle, 30));
+            $containerTemplateHtml[] =    htmlspecialchars(GeneralUtility::fixed_lgd_cs($containerTitle, 30));
             $containerTemplateHtml[] = '</a>';
             $containerTemplatesHtml[] = implode(LF, $containerTemplateHtml);
-
-            $flexFormContainerContainerTemplateResult['html'] = '';
-            $flexFormContainerContainerTemplateResult['additionalJavaScriptPost'] = [];
-            $flexFormContainerContainerTemplateResult['additionalJavaScriptSubmit'] = [];
-
-            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $flexFormContainerContainerTemplateResult);
         }
-
         // Create new elements links
         $createElementsHtml = [];
         if ($userHasAccessToDefaultLanguage) {
             $createElementsHtml[] = '<div class="t3-form-field-add-flexsection">';
             $createElementsHtml[] =    '<div class="btn-group">';
-            $createElementsHtml[] =        implode('|', $containerTemplatesHtml);
+            $createElementsHtml[] =        implode('', $containerTemplatesHtml);
             $createElementsHtml[] =    '</div>';
             $createElementsHtml[] = '</div>';
         }
 
+        $sectionTitle = '';
+        if (!empty(trim($flexFormDataStructureArray['title']))) {
+            $sectionTitle = $languageService->sL(trim($flexFormDataStructureArray['title']));
+        }
+
         // Wrap child stuff
         $toggleAll = htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.toggleall'));
         $html = [];
@@ -173,7 +120,7 @@ class FlexFormSectionContainer extends AbstractContainer
         $html[] =         '<div class="t3-form-field-container t3-form-flex">';
         $html[] =             '<div class="t3-form-field-label-flexsection">';
         $html[] =                 '<h4>';
-        $html[] =                     htmlspecialchars($flexFormSectionTitle);
+        $html[] =                     htmlspecialchars($sectionTitle);
         $html[] =                 '</h4>';
         $html[] =             '</div>';
         $html[] =             '<div class="t3js-form-field-toggle-flexsection t3-form-flexsection-toggle">';
@@ -182,7 +129,6 @@ class FlexFormSectionContainer extends AbstractContainer
         $html[] =                 '</a>';
         $html[] =             '</div>';
         $html[] =             '<div';
-        $html[] =                 'id="' . $flexFormFieldIdentifierPrefix . '"';
         $html[] =                 'class="panel-group panel-hover t3-form-field-container-flexsection t3-flex-container"';
         $html[] =                 'data-t3-flex-allow-restructure="' . ($userHasAccessToDefaultLanguage ? '1' : '0') . '"';
         $html[] =             '>';
index d03cca6..b937ae8 100644 (file)
@@ -72,6 +72,7 @@ class FlexFormTabsContainer extends AbstractContainer
             $options = $this->data;
             $options['flexFormDataStructureArray'] = $sheetDataStructure['ROOT']['el'];
             $options['flexFormRowData'] = $flexFormRowSheetDataSubPart;
+            $options['flexFormSheetName'] = $sheetName;
             $options['flexFormFormPrefix'] = '[data][' . $sheetName . '][lDEF]';
             $options['parameterArray'] = $parameterArray;
             // Merge elements of this tab into a single list again and hand over to
index 2ac0fd3..fa24fa8 100644 (file)
@@ -123,9 +123,9 @@ class InlineControlContainer extends AbstractContainer
 
         // Transport the flexform DS identifier fields to the FormAjaxInlineController
         if (!empty($newStructureItem['flexform'])
-            && isset($this->data['processedTca']['columns'][$field]['config']['ds']['meta']['dataStructurePointers'])
+            && isset($this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'])
         ) {
-            $config['flexDataStructurePointers'] = $this->data['processedTca']['columns'][$field]['config']['ds']['meta']['dataStructurePointers'];
+            $config['flexDataStructureIdentifier'] = $this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'];
         }
 
         // e.g. data[<table>][<uid>][<field>]
@@ -134,10 +134,6 @@ class InlineControlContainer extends AbstractContainer
         $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
 
         $config['inline']['first'] = false;
-        // @todo: This initialization shouldn't be required data provider should take care this is set?
-        if (!is_array($this->data['parameterArray']['fieldConf']['children'])) {
-            $this->data['parameterArray']['fieldConf']['children'] = [];
-        }
         $firstChild = reset($this->data['parameterArray']['fieldConf']['children']);
         if (isset($firstChild['databaseRow']['uid'])) {
             $config['inline']['first'] = $firstChild['databaseRow']['uid'];
index 32a2974..c057670 100644 (file)
@@ -323,8 +323,7 @@ class InlineRecordContainer extends AbstractContainer
         // Renders a thumbnail for the header
         if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] && !empty($inlineConfig['appearance']['headerThumbnail']['field'])) {
             $fieldValue = $rec[$inlineConfig['appearance']['headerThumbnail']['field']];
-            $firstElement = array_shift(GeneralUtility::trimExplode('|', array_shift(GeneralUtility::trimExplode(',', $fieldValue))));
-            $fileUid = array_pop(BackendUtility::splitTable_Uid($firstElement));
+            $fileUid = $fieldValue[0]['uid'];
 
             if (!empty($fileUid)) {
                 try {
@@ -419,14 +418,15 @@ class InlineRecordContainer extends AbstractContainer
                 . '</span>';
         }
         // "Info": (All records)
+        // @todo: hardcoded sys_file!
+        if ($rec['table_local'] === 'sys_file') {
+            $uid = $rec['uid_local'][0]['uid'];
+            $table = '_FILE';
+        } else {
+            $uid = $rec['uid'];
+            $table = $foreignTable;
+        }
         if ($enabledControls['info'] && !$isNewItem) {
-            if ($rec['table_local'] === 'sys_file') {
-                $uid = (int)substr($rec['uid_local'], 9);
-                $table = '_FILE';
-            } else {
-                $uid = $rec['uid'];
-                $table = $foreignTable;
-            }
             $cells['info'] = '
                                <a class="btn btn-default" href="#" onclick="' . htmlspecialchars(('top.launchView(' . GeneralUtility::quoteJSvalue($table) . ', ' . GeneralUtility::quoteJSvalue($uid) . '); return false;')) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:showInfo')) . '">
                                        ' . $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() . '
@@ -479,7 +479,7 @@ class InlineRecordContainer extends AbstractContainer
                     ->where(
                         $queryBuilder->expr()->eq(
                             'file',
-                            $queryBuilder->createNamedParameter(substr($rec['uid_local'], 9), \PDO::PARAM_INT)
+                            $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
                         ),
                         $queryBuilder->expr()->eq(
                             'sys_language_uid',
index 21d3ac6..6dbfa06 100644 (file)
@@ -40,6 +40,7 @@ class SingleFieldContainer extends AbstractContainer
     /**
      * Entry method
      *
+     * @throws \InvalidArgumentException
      * @return array As defined in initializeResultArray() of AbstractNode
      */
     public function render()
@@ -71,19 +72,14 @@ class SingleFieldContainer extends AbstractContainer
             if (MathUtility::canBeInterpretedAsInteger($parentValue)) {
                 $isOverlay = (bool)$parentValue;
             } elseif (is_array($parentValue)) {
-                // This case may apply if the value has been converted to an array by the select data provider
+                // This case may apply if the value has been converted to an array by the select or group data provider
                 $isOverlay = !empty($parentValue) ? (bool)$parentValue[0] : false;
-            } elseif (is_string($parentValue) && $parentValue !== '') {
-                // This case may apply if a group definition is used in TCA and the group provider builds a weird string
-                $recordsReferencedInField = GeneralUtility::trimExplode(',', $parentValue);
-                // Pick the first record because if you set multiple records you're in trouble anyways
-                $recordIdentifierParts = GeneralUtility::trimExplode('|', $recordsReferencedInField[0]);
-                list(, $refUid) = BackendUtility::splitTable_Uid($recordIdentifierParts[0]);
-                $isOverlay = MathUtility::canBeInterpretedAsInteger($refUid) ? (bool)$refUid : false;
             } else {
-                throw new \InvalidArgumentException('The given value for the original language field '
-                                                    . $this->data['processedTca']['ctrl']['transOrigPointerField']
-                                                    . ' of table ' . $table . ' contains an invalid value.', 1470742770);
+                throw new \InvalidArgumentException(
+                    'The given value for the original language field ' . $this->data['processedTca']['ctrl']['transOrigPointerField']
+                    . ' of table ' . $table . ' contains an invalid value.',
+                    1470742770
+                );
             }
         }
 
@@ -134,14 +130,19 @@ class SingleFieldContainer extends AbstractContainer
             $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)
+        // This is used for eg. "type" fields and others configured with "onChange"
+        if (!empty($this->data['processedTca']['ctrl']['type']) && $fieldName === $typeField
+            || isset($parameterArray['fieldConf']['onChange']) && $parameterArray['fieldConf']['onChange'] === 'reload'
         ) {
             if ($backendUser->jsConfirmation(JsConfirmation::TYPE_CHANGE)) {
-                $alertMsgOnChange = 'top.TYPO3.Modal.confirm(TYPO3.lang["FormEngine.refreshRequiredTitle"], TYPO3.lang["FormEngine.refreshRequiredContent"]).on("button.clicked", function(e) { if (e.target.name == "ok" && TBE_EDITOR.checkSubmit(-1)) { TBE_EDITOR.submitForm() } top.TYPO3.Modal.dismiss(); });';
+                $alertMsgOnChange = 'top.TYPO3.Modal.confirm('
+                        . 'TYPO3.lang["FormEngine.refreshRequiredTitle"],'
+                        . ' TYPO3.lang["FormEngine.refreshRequiredContent"]'
+                    . ')'
+                    . '.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() };';
             }
index 9f3d460..e528480 100644 (file)
@@ -17,6 +17,8 @@ namespace TYPO3\CMS\Backend\Form;
 /**
  * Interface for classes which hook into \TYPO3\CMS\Backend\Form\FormEngine
  * and do additional dbFileIcons processing
+ *
+ * @deprecated and no longer called since TYPO3 v8, will be removed in TYPO3 v9
  */
 interface DatabaseFileIconsHookInterface
 {
index 5fe64b0..883ed97 100644 (file)
@@ -14,21 +14,16 @@ namespace TYPO3\CMS\Backend\Form\Element;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Backend\Clipboard\Clipboard;
 use TYPO3\CMS\Backend\Form\AbstractNode;
-use TYPO3\CMS\Backend\Form\DatabaseFileIconsHookInterface;
 use TYPO3\CMS\Backend\Form\FormDataCompiler;
 use TYPO3\CMS\Backend\Form\FormDataGroup\OnTheFly;
 use TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectItems;
-use TYPO3\CMS\Backend\Form\InlineStackProcessor;
 use TYPO3\CMS\Backend\Form\NodeFactory;
 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
 use TYPO3\CMS\Backend\Form\Wizard\SuggestWizard;
 use TYPO3\CMS\Backend\Form\Wizard\ValueSliderWizard;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
-use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Imaging\IconFactory;
-use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
@@ -61,11 +56,6 @@ abstract class AbstractFormElement extends AbstractNode
     protected $maxInputWidth = 50;
 
     /**
-     * @var \TYPO3\CMS\Backend\Clipboard\Clipboard|NULL
-     */
-    protected $clipboard = null;
-
-    /**
      * @var NodeFactory
      */
     protected $nodeFactory;
@@ -387,15 +377,8 @@ abstract class AbstractFormElement extends AbstractNode
                     if (!empty($PA['fieldTSConfig']['suggest.']['default.']['hide'])) {
                         break;
                     }
-                    // The suggest wizard needs to know if we're in flex form scope to use the dataStructureIdentifier.
-                    // If so, add the processedTca of the flex config as wizard argument.
-                    $flexFormConfig = [];
-                    if ($this->data['processedTca']['columns'][$field]['config']['type'] === 'flex') {
-                        $flexFormConfig = $this->data['processedTca']['columns'][$field];
-                    }
-                    /** @var SuggestWizard $suggestWizard */
                     $suggestWizard = GeneralUtility::makeInstance(SuggestWizard::class);
-                    $otherWizards[] = $suggestWizard->renderSuggestSelector($PA['itemFormElName'], $table, $field, $row, $PA, $flexFormConfig);
+                    $otherWizards[] = $suggestWizard->renderSuggestSelector($this->data);
                     break;
             }
         }
@@ -433,398 +416,6 @@ abstract class AbstractFormElement extends AbstractNode
     }
 
     /**
-     * Prints the selector box form-field for the db/file/select elements (multiple)
-     *
-     * @param string $fName Form element name
-     * @param string $mode Mode "db", "file" (internal_type for the "group" type) OR blank (then for the "select" type)
-     * @param string $allowed Commalist of "allowed
-     * @param array $itemArray The array of items. For "select" and "group"/"file" this is just a set of value. For "db" its an array of arrays with table/uid pairs.
-     * @param string $selector Alternative selector box.
-     * @param array $params An array of additional parameters, eg: "size", "info", "headers" (array with "selector" and "items"), "noBrowser", "thumbnails
-     * @param null $_ unused (onFocus in the past), will be removed in TYPO3 CMS 9
-     * @param string $table (optional) Table name processing for
-     * @param string $field (optional) Field of table name processing for
-     * @param string $uid (optional) uid of table record processing for
-     * @param array $config (optional) The TCA field config
-     * @return string The form fields for the selection.
-     * @throws \UnexpectedValueException
-     * @todo: Hack this mess into pieces and inline to group / select element depending on what they need
-     */
-    protected function dbFileIcons($fName, $mode, $allowed, $itemArray, $selector = '', $params = [], $_ = null, $table = '', $field = '', $uid = '', $config = [])
-    {
-        $languageService = $this->getLanguageService();
-        $disabled = '';
-        if ($params['readOnly']) {
-            $disabled = ' disabled="disabled"';
-        }
-        // INIT
-        $uidList = [];
-        $opt = [];
-        $itemArrayC = 0;
-        // Creating <option> elements:
-        if (is_array($itemArray)) {
-            $itemArrayC = count($itemArray);
-            switch ($mode) {
-                case 'db':
-                    foreach ($itemArray as $pp) {
-                        $pRec = BackendUtility::getRecordWSOL($pp['table'], $pp['id']);
-                        if (is_array($pRec)) {
-                            $pTitle = BackendUtility::getRecordTitle($pp['table'], $pRec, false, true);
-                            $pUid = $pp['table'] . '_' . $pp['id'];
-                            $uidList[] = $pUid;
-                            $title = htmlspecialchars($pTitle);
-                            $opt[] = '<option value="' . htmlspecialchars($pUid) . '" title="' . $title . '">' . $title . '</option>';
-                        }
-                    }
-                    break;
-                case 'file_reference':
-
-                case 'file':
-                    foreach ($itemArray as $item) {
-                        $itemParts = explode('|', $item);
-                        $uidList[] = ($pUid = ($pTitle = $itemParts[0]));
-                        $title = htmlspecialchars(rawurldecode($itemParts[1]));
-                        $opt[] = '<option value="' . htmlspecialchars(rawurldecode($itemParts[0])) . '" title="' . $title . '">' . $title . '</option>';
-                    }
-                    break;
-                case 'folder':
-                    foreach ($itemArray as $pp) {
-                        $pParts = explode('|', $pp);
-                        $uidList[] = ($pUid = ($pTitle = $pParts[0]));
-                        $title = htmlspecialchars(rawurldecode($pParts[0]));
-                        $opt[] = '<option value="' . htmlspecialchars(rawurldecode($pParts[0])) . '" title="' . $title . '">' . $title . '</option>';
-                    }
-                    break;
-                default:
-                    foreach ($itemArray as $pp) {
-                        $pParts = explode('|', $pp, 2);
-                        $uidList[] = ($pUid = $pParts[0]);
-                        $pTitle = $pParts[1];
-                        $title = htmlspecialchars(rawurldecode($pTitle));
-                        $opt[] = '<option value="' . htmlspecialchars(rawurldecode($pUid)) . '" title="' . $title . '">' . $title . '</option>';
-                    }
-            }
-        }
-        // Create selector box of the options
-        $sSize = $params['autoSizeMax']
-            ? MathUtility::forceIntegerInRange($itemArrayC + 1, MathUtility::forceIntegerInRange($params['size'], 1), $params['autoSizeMax'])
-            : $params['size'];
-        if (!$selector) {
-            $maxItems = (int)($params['maxitems'] ?? 0);
-            $size = (int)($params['size'] ?? 0);
-            $classes = ['form-control', 'tceforms-multiselect'];
-            if ($maxItems === 1) {
-                $classes[] = 'form-select-no-siblings';
-            }
-            $isMultiple = $maxItems !== 1 && $size !== 1;
-            $selector = '<select id="' . StringUtility::getUniqueId('tceforms-multiselect-') . '" '
-                . ($params['noList'] ? 'style="display: none"' : 'size="' . $sSize . '" class="' . implode(' ', $classes) . '"')
-                . ($isMultiple ? ' multiple="multiple"' : '')
-                . ' data-formengine-input-name="' . htmlspecialchars($fName) . '" ' . $this->getValidationDataAsDataAttribute($config) . $params['style'] . $disabled . '>' . implode('', $opt)
-                . '</select>';
-        }
-        $icons = [
-            'L' => [],
-            'R' => []
-        ];
-        $rOnClickInline = '';
-        if (!$params['readOnly'] && !$params['noList']) {
-            if (!$params['noBrowser']) {
-                // Check against inline uniqueness
-                /** @var InlineStackProcessor $inlineStackProcessor */
-                $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
-                $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
-                $aOnClickInline = '';
-                if ($this->data['isInlineChild'] && $this->data['inlineParentUid']) {
-                    if ($this->data['inlineParentConfig']['foreign_table'] === $table
-                        && $this->data['inlineParentConfig']['foreign_unique'] === $field
-                    ) {
-                        $objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']) . '-' . $table;
-                        $aOnClickInline = $objectPrefix . '|inline.checkUniqueElement|inline.setUniqueElement';
-                        $rOnClickInline = 'inline.revertUnique(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',null,' . GeneralUtility::quoteJSvalue($uid) . ');';
-                    }
-                }
-                if (is_array($config['appearance']) && isset($config['appearance']['elementBrowserType'])) {
-                    $elementBrowserType = $config['appearance']['elementBrowserType'];
-                } else {
-                    $elementBrowserType = $mode;
-                }
-                if (is_array($config['appearance']) && isset($config['appearance']['elementBrowserAllowed'])) {
-                    $elementBrowserAllowed = $config['appearance']['elementBrowserAllowed'];
-                } else {
-                    $elementBrowserAllowed = $allowed;
-                }
-                $aOnClick = 'setFormValueOpenBrowser(' . GeneralUtility::quoteJSvalue($elementBrowserType) . ','
-                    . GeneralUtility::quoteJSvalue(($fName . '|||' . $elementBrowserAllowed . '|' . $aOnClickInline)) . '); return false;';
-                $icons['R'][] = '
-                                       <a href="#"
-                                               onclick="' . htmlspecialchars($aOnClick) . '"
-                                               class="btn btn-default"
-                                               title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.browse_' . ($mode == 'db' ? 'db' : 'file'))) . '">
-                                               ' . $this->iconFactory->getIcon('actions-insert-record', Icon::SIZE_SMALL)->render() . '
-                                       </a>';
-            }
-            if (!$params['dontShowMoveIcons']) {
-                if ($sSize >= 5) {
-                    $icons['L'][] = '
-                                               <a href="#"
-                                                       class="btn btn-default t3js-btn-moveoption-top"
-                                                       data-fieldname="' . $fName . '"
-                                                       title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move_to_top')) . '">
-                                                       ' . $this->iconFactory->getIcon('actions-move-to-top', Icon::SIZE_SMALL)->render() . '
-                                               </a>';
-                }
-                $icons['L'][] = '
-                                       <a href="#"
-                                               class="btn btn-default t3js-btn-moveoption-up"
-                                               data-fieldname="' . $fName . '"
-                                               title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move_up')) . '">
-                                               ' . $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render() . '
-                                       </a>';
-                $icons['L'][] = '
-                                       <a href="#"
-                                               class="btn btn-default t3js-btn-moveoption-down"
-                                               data-fieldname="' . $fName . '"
-                                               title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move_down')) . '">
-                                               ' . $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render() . '
-                                       </a>';
-                if ($sSize >= 5) {
-                    $icons['L'][] = '
-                                               <a href="#"
-                                                       class="btn btn-default t3js-btn-moveoption-bottom"
-                                                       data-fieldname="' . $fName . '"
-                                                       title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move_to_bottom')) . '">
-                                                       ' . $this->iconFactory->getIcon('actions-move-to-bottom', Icon::SIZE_SMALL)->render() . '
-                                               </a>';
-                }
-            }
-            $clipElements = $this->getClipboardElements($allowed, $mode);
-            if (!empty($clipElements)) {
-                $aOnClick = '';
-                foreach ($clipElements as $elValue) {
-                    if ($mode == 'db') {
-                        list($itemTable, $itemUid) = explode('|', $elValue);
-                        $recordTitle = BackendUtility::getRecordTitle($itemTable, BackendUtility::getRecordWSOL($itemTable, $itemUid));
-                        $itemTitle = GeneralUtility::quoteJSvalue($recordTitle);
-                        $elValue = $itemTable . '_' . $itemUid;
-                    } else {
-                        // 'file', 'file_reference' and 'folder' mode
-                        $itemTitle = 'unescape(' . GeneralUtility::quoteJSvalue(rawurlencode(basename($elValue))) . ')';
-                    }
-                    $aOnClick .= 'setFormValueFromBrowseWin(' . GeneralUtility::quoteJSvalue($fName) . ',unescape('
-                        . GeneralUtility::quoteJSvalue(rawurlencode(str_replace('%20', ' ', $elValue))) . '),' . $itemTitle . ',' . $itemTitle . ');';
-                }
-                $aOnClick .= 'return false;';
-                $icons['R'][] = '
-                                       <a href="#"
-                                               class="btn btn-default"
-                                               onclick="' . htmlspecialchars($aOnClick) . '"
-                                               title="' . htmlspecialchars(sprintf($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.clipInsert_' . ($mode == 'db' ? 'db' : 'file')), count($clipElements))) . '">
-                                               ' . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render() . '
-                                       </a>';
-            }
-        }
-        if (!$params['readOnly'] && !$params['noDelete']) {
-            $icons['L'][] = '
-                               <a href="#"
-                                       class="btn btn-default t3js-btn-removeoption"
-                                       onClick="' . $rOnClickInline . '"
-                                       data-fieldname="' . $fName . '"
-                                       title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.remove_selected')) . '">
-                                       ' . $this->iconFactory->getIcon('actions-selection-delete', Icon::SIZE_SMALL)->render() . '
-                               </a>';
-        }
-
-        // Thumbnails
-        $imagesOnly = false;
-        if ($params['thumbnails'] && $params['allowed']) {
-            // In case we have thumbnails, check if only images are allowed.
-            // In this case, render them below the field, instead of to the right
-            $allowedExtensionList = $params['allowed'];
-            $imageExtensionList = GeneralUtility::trimExplode(',', strtolower($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']), true);
-            $imagesOnly = true;
-            foreach ($allowedExtensionList as $allowedExtension) {
-                if (!ArrayUtility::inArray($imageExtensionList, $allowedExtension)) {
-                    $imagesOnly = false;
-                    break;
-                }
-            }
-        }
-        $thumbnails = '';
-        if (is_array($params['thumbnails']) && !empty($params['thumbnails'])) {
-            if ($imagesOnly) {
-                $thumbnails .= '<ul class="list-inline">';
-                foreach ($params['thumbnails'] as $thumbnail) {
-                    $thumbnails .= '<li><span class="thumbnail">' . $thumbnail['image'] . '</span></li>';
-                }
-                $thumbnails .= '</ul>';
-            } else {
-                $thumbnails .= '<div class="table-fit"><table class="table table-white"><tbody>';
-                foreach ($params['thumbnails'] as $thumbnail) {
-                    $thumbnails .= '
-                                               <tr>
-                                                       <td class="col-icon">
-                                                               ' . ($config['internal_type'] === 'db'
-                            ? BackendUtility::wrapClickMenuOnIcon($thumbnail['image'], $thumbnail['table'], $thumbnail['uid'], 1, '', '+copy,info,edit,view')
-                            : $thumbnail['image']) . '
-                                                       </td>
-                                                       <td class="col-title">
-                                                               ' . ($config['internal_type'] === 'db'
-                            ? BackendUtility::wrapClickMenuOnIcon($thumbnail['name'], $thumbnail['table'], $thumbnail['uid'], 1, '', '+copy,info,edit,view')
-                            : $thumbnail['name']) . '
-                                                               ' . ($config['internal_type'] === 'db' ? ' <span class="text-muted">[' . $thumbnail['uid'] . ']</span>' : '') . '
-                                                       </td>
-                                               </tr>
-                                               ';
-                }
-                $thumbnails .= '</tbody></table></div>';
-            }
-        }
-
-        // Allowed Tables
-        $allowedTables = '';
-        if (is_array($params['allowedTables']) && !empty($params['allowedTables']) && !$params['hideAllowedTables']) {
-            $allowedTables .= '<div class="help-block">';
-            foreach ($params['allowedTables'] as $key => $item) {
-                if (is_array($item)) {
-                    if (empty($params['readOnly'])) {
-                        $allowedTables .= '<a href="#" onClick="' . htmlspecialchars($item['onClick']) . '" class="btn btn-default">' . $item['icon'] . ' ' . htmlspecialchars($item['name']) . '</a> ';
-                    } else {
-                        $allowedTables .= '<span>' . htmlspecialchars($item['name']) . '</span> ';
-                    }
-                } elseif ($key === 'name') {
-                    $allowedTables .= '<span>' . htmlspecialchars($item) . '</span> ';
-                }
-            }
-            $allowedTables .= '</div>';
-        }
-        // Allowed
-        $allowedList = '';
-        if (is_array($params['allowed']) && !empty($params['allowed'])) {
-            foreach ($params['allowed'] as $item) {
-                $allowedList .= '<span class="label label-success">' . strtoupper($item) . '</span> ';
-            }
-        }
-        // Disallowed
-        $disallowedList = '';
-        if (is_array($params['disallowed']) && !empty($params['disallowed'])) {
-            foreach ($params['disallowed'] as $item) {
-                $disallowedList .= '<span class="label label-danger">' . strtoupper($item) . '</span> ';
-            }
-        }
-        // Rightbox
-        $rightbox = ($params['rightbox'] ?: '');
-
-        // Hook: dbFileIcons_postProcess (requested by FAL-team for use with the "fal" extension)
-        if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms.php']['dbFileIcons'])) {
-            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms.php']['dbFileIcons'] as $classRef) {
-                $hookObject = GeneralUtility::getUserObj($classRef);
-                if (!$hookObject instanceof DatabaseFileIconsHookInterface) {
-                    throw new \UnexpectedValueException($classRef . ' must implement interface ' . DatabaseFileIconsHookInterface::class, 1290167704);
-                }
-                $additionalParams = [
-                    'mode' => $mode,
-                    'allowed' => $allowed,
-                    'itemArray' => $itemArray,
-                    'table' => $table,
-                    'field' => $field,
-                    'uid' => $uid,
-                    'config' => $GLOBALS['TCA'][$table]['columns'][$field]
-                ];
-                $hookObject->dbFileIcons_postProcess($params, $selector, $thumbnails, $icons, $rightbox, $fName, $uidList, $additionalParams, $this);
-            }
-        }
-
-        // Output
-        $str = '
-                       ' . ($params['headers']['selector'] ? '<label>' . $params['headers']['selector'] . '</label>' : '') . '
-                       <div class="form-wizards-wrap form-wizards-aside">
-                               <div class="form-wizards-element">
-                                       ' . $selector . '
-                                       ' . (!$params['noList'] && !empty($allowedTables) ? $allowedTables : '') . '
-                                       ' . (!$params['noList'] && (!empty($allowedList) || !empty($disallowedList))
-                ? '<div class="help-block">' . $allowedList . $disallowedList . ' </div>'
-                : '') . '
-                               </div>
-                               ' . (!empty($icons['L']) ? '<div class="form-wizards-items"><div class="btn-group-vertical">' . implode('', $icons['L']) . '</div></div>' : '') . '
-                               ' . (!empty($icons['R']) ? '<div class="form-wizards-items"><div class="btn-group-vertical">' . implode('', $icons['R']) . '</div></div>' : '') . '
-                       </div>
-                       ';
-        if ($rightbox) {
-            $str = '
-                               <div class="form-multigroup-wrap t3js-formengine-field-group">
-                                       <div class="form-multigroup-item form-multigroup-element">' . $str . '</div>
-                                       <div class="form-multigroup-item form-multigroup-element">
-                                               ' . ($params['headers']['items'] ? '<label>' . $params['headers']['items'] . '</label>' : '') . '
-                                               ' . ($params['headers']['selectorbox'] ? '<div class="form-multigroup-item-wizard">' . $params['headers']['selectorbox'] . '</div>' : '') . '
-                                               ' . $rightbox . '
-                                       </div>
-                               </div>
-                               ';
-        }
-        $str .= $thumbnails;
-
-        // Creating the hidden field which contains the actual value as a comma list.
-        $str .= '<input type="hidden" name="' . $fName . '" value="' . htmlspecialchars(implode(',', $uidList)) . '" />';
-        return $str;
-    }
-
-    /**
-     * Returns array of elements from clipboard to insert into GROUP element box.
-     *
-     * @param string $allowed Allowed elements, Eg "pages,tt_content", "gif,jpg,jpeg,png
-     * @param string $mode Mode of relations: "db" or "file
-     * @return array Array of elements in values (keys are insignificant), if none found, empty array.
-     */
-    protected function getClipboardElements($allowed, $mode)
-    {
-        if (!is_object($this->clipboard)) {
-            $this->clipboard = GeneralUtility::makeInstance(Clipboard::class);
-            $this->clipboard->initializeClipboard();
-        }
-
-        $output = [];
-        switch ($mode) {
-            case 'file_reference':
-
-            case 'file':
-                $elFromTable = $this->clipboard->elFromTable('_FILE');
-                $allowedExts = GeneralUtility::trimExplode(',', $allowed, true);
-                // If there are a set of allowed extensions, filter the content:
-                if ($allowedExts) {
-                    foreach ($elFromTable as $elValue) {
-                        $pI = pathinfo($elValue);
-                        $ext = strtolower($pI['extension']);
-                        if (in_array($ext, $allowedExts)) {
-                            $output[] = $elValue;
-                        }
-                    }
-                } else {
-                    // If all is allowed, insert all: (This does NOT respect any disallowed extensions,
-                    // but those will be filtered away by the backend DataHandler)
-                    $output = $elFromTable;
-                }
-                break;
-            case 'db':
-                $allowedTables = GeneralUtility::trimExplode(',', $allowed, true);
-                // All tables allowed for relation:
-                if (trim($allowedTables[0]) === '*') {
-                    $output = $this->clipboard->elFromTable('');
-                } else {
-                    // Only some tables, filter them:
-                    foreach ($allowedTables as $tablename) {
-                        $elFromTable = $this->clipboard->elFromTable($tablename);
-                        $output = array_merge($output, $elFromTable);
-                    }
-                }
-                $output = array_keys($output);
-                break;
-        }
-
-        return $output;
-    }
-
-    /**
      * @return LanguageService
      */
     protected function getLanguageService()
index 9abbac7..8fe96d8 100644 (file)
@@ -14,6 +14,8 @@ namespace TYPO3\CMS\Backend\Form\Element;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Backend\Clipboard\Clipboard;
+use TYPO3\CMS\Backend\Form\InlineStackProcessor;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Imaging\Icon;
@@ -23,6 +25,7 @@ use TYPO3\CMS\Core\Resource\ProcessedFile;
 use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
+use TYPO3\CMS\Core\Utility\StringUtility;
 
 /**
  * Generation of TCEform elements of the type "group"
@@ -30,6 +33,11 @@ use TYPO3\CMS\Core\Utility\MathUtility;
 class GroupElement extends AbstractFormElement
 {
     /**
+     * @var Clipboard
+     */
+    protected $clipboard;
+
+    /**
      * This will render a selector box into which elements from either
      * the file system or database can be inserted. Relations.
      *
@@ -38,298 +46,478 @@ class GroupElement extends AbstractFormElement
      */
     public function render()
     {
+        $languageService = $this->getLanguageService();
+        $backendUser = $this->getBackendUserAuthentication();
+
         $table = $this->data['tableName'];
         $fieldName = $this->data['fieldName'];
         $row = $this->data['databaseRow'];
         $parameterArray = $this->data['parameterArray'];
         $config = $parameterArray['fieldConf']['config'];
-        $show_thumbs = $config['show_thumbs'];
+        $elementName = $parameterArray['itemFormElName'];
+
         $resultArray = $this->initializeResultArray();
 
-        $size = isset($config['size']) ? (int)$config['size'] : $this->minimumInputWidth;
-        $maxitems = MathUtility::forceIntegerInRange($config['maxitems'], 0);
-        if (!$maxitems) {
-            $maxitems = 100000;
+        $selectedItems = $parameterArray['itemFormElValue'];
+        $selectedItemsCount = count($selectedItems);
+
+        $maxItems = $config['maxitems'];
+        $autoSizeMax = MathUtility::forceIntegerInRange($config['autoSizeMax'], 0);
+        $size = 5;
+        if (isset($config['size'])) {
+            $size = (int)$config['size'];
+        }
+        if ($autoSizeMax >= 1) {
+            $size = MathUtility::forceIntegerInRange($selectedItemsCount + 1, MathUtility::forceIntegerInRange($size, 1), $autoSizeMax);
         }
-        $minitems = MathUtility::forceIntegerInRange($config['minitems'], 0);
-        $thumbnails = [];
-        $allowed = GeneralUtility::trimExplode(',', $config['allowed'], true);
-        $disallowed = GeneralUtility::trimExplode(',', $config['disallowed'], true);
-        $disabled = $config['readOnly'];
-        $info = [];
-        $parameterArray['itemFormElID_file'] = $parameterArray['itemFormElID'] . '_files';
 
-        // whether the list and delete controls should be disabled
-        $noList = isset($config['disable_controls']) && GeneralUtility::inList($config['disable_controls'], 'list');
-        $noDelete = isset($config['disable_controls']) && GeneralUtility::inList($config['disable_controls'], 'delete');
+        $isDisabled = false;
+        if (isset($config['readOnly']) && $config['readOnly']) {
+            $isDisabled = true;
+        }
+        $showMoveIcons = true;
+        if (isset($config['hideMoveIcons']) && $config['hideMoveIcons']) {
+            $showMoveIcons = false;
+        }
 
-        // "Extra" configuration; Returns configuration for the field based on settings found in the "types" fieldlist.
-        $specConf = BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras']);
+        $internalType = (string)$config['internal_type'];
+        $showThumbs = (bool)$config['show_thumbs'];
+        $allowed = GeneralUtility::trimExplode(',', $config['allowed'], true);
+        $disallowed = GeneralUtility::trimExplode(',', $config['disallowed'], true);
+        $uploadFieldId = $parameterArray['itemFormElID'] . '_files';
+        $itemCanBeSelectedMoreThanOnce = !empty($config['multiple']);
+        $maxTitleLength = $backendUser->uc['titleLen'];
+        $isDirectFileUploadEnabled = (bool)$backendUser->uc['edit_docModuleUpload'];
+        $clipboardElements = $config['clipboardElements'];
 
-        // Register properties in required elements / validation
-        $attributes['data-formengine-validation-rules'] = htmlspecialchars(
-            $this->getValidationDataAsJsonString(
-                [
-                    'minitems' => $minitems,
-                    'maxitems' => $maxitems
-                ]
-            )
-        );
+        $disableControls = [];
+        if (isset($config['disable_controls'])) {
+            $disableControls = GeneralUtility::trimExplode(',', $config['disable_controls'], true);
+        }
+        $showListControl = true;
+        if (in_array('list', $disableControls, true)) {
+            $showListControl = false;
+        }
+        $showDeleteControl = true;
+        if (in_array('delete', $disableControls, true)) {
+            $showDeleteControl = false;
+        }
+        $showBrowseControl = true;
+        if (in_array('browser', $disableControls, true)) {
+            $showBrowseControl = false;
+        }
+        $showAllowedTables = true;
+        if (in_array('allowedTables', $disableControls, true)) {
+            $showAllowedTables = false;
+        }
+        $showUploadField = true;
+        if (in_array('upload', $disableControls, true)) {
+            $showUploadField = false;
+        }
 
-        // If maxitems==1 then automatically replace the current item (in list and file selector)
-        if ($maxitems === 1) {
+        if ($maxItems === 1) {
+            // If maxitems==1 then automatically replace the current item (in list and file selector)
             $resultArray['additionalJavaScriptPost'][] =
-                'TBE_EDITOR.clearBeforeSettingFormValueFromBrowseWin[' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . '] = {
-                                       itemFormElID_file: ' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElID_file']) . '
-                               }';
-            $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = 'setFormValueManipulate(' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName'])
-                . ', \'Remove\'); ' . $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'];
-        } elseif ($noList) {
+                'TBE_EDITOR.clearBeforeSettingFormValueFromBrowseWin[' . GeneralUtility::quoteJSvalue($elementName) . '] = {'
+                    . 'itemFormElID_file: ' . GeneralUtility::quoteJSvalue($uploadFieldId)
+                . '}';
+            $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] =
+                'setFormValueManipulate(' . GeneralUtility::quoteJSvalue($elementName) . ', \'Remove\');'
+                . $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'];
+        } elseif (!$showListControl) {
             // If the list controls have been removed and the maximum number is reached, remove the first entry to avoid "write once" field
-            $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = 'setFormValueManipulate(' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName'])
-                . ', \'RemoveFirstIfFull\', ' . GeneralUtility::quoteJSvalue($maxitems) . '); ' . $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'];
+            $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] =
+                'setFormValueManipulate(' . GeneralUtility::quoteJSvalue($elementName) . ', \'RemoveFirstIfFull\', ' . $maxItems . ');'
+                . $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'];
         }
 
-        $html = '<input type="hidden" class="t3js-group-hidden-field" data-formengine-input-name="' . htmlspecialchars($parameterArray['itemFormElName']) . '" value="' . ($config['multiple'] ? 1 : 0) . '"' . $disabled . ' />';
-
-        // Define parameters for all types below
-        $commonParameters = [
-            'size' => $size,
-            'dontShowMoveIcons' => isset($config['hideMoveIcons']) || $maxitems <= 1,
-            'autoSizeMax' => MathUtility::forceIntegerInRange($config['autoSizeMax'], 0),
-            'maxitems' => $maxitems,
-            'style' => isset($config['selectedListStyle'])
-                ? ' style="' . htmlspecialchars($config['selectedListStyle']) . '"'
-                : '',
-            'readOnly' => $disabled,
-            'noBrowser' => $noList || isset($config['disable_controls']) && GeneralUtility::inList($config['disable_controls'], 'browser'),
-            'noList' => $noList,
-            'hideAllowedTables' => GeneralUtility::inList($config['disable_controls'], 'allowedTables'),
-        ];
-
-        // Acting according to either "file" or "db" type:
-        switch ((string)$config['internal_type']) {
-            case 'file_reference':
-                $config['uploadfolder'] = '';
-                // Fall through
-            case 'file':
-                // Creating string showing allowed types:
-                if (empty($allowed)) {
-                    $allowed = ['*'];
-                }
-                // Making the array of file items:
-                $itemArray = GeneralUtility::trimExplode(',', $parameterArray['itemFormElValue'], true);
-                $fileFactory = ResourceFactory::getInstance();
-                // Correct the filename for the FAL items
-                foreach ($itemArray as &$fileItem) {
-                    list($fileUid, $fileLabel) = explode('|', $fileItem);
-                    if (MathUtility::canBeInterpretedAsInteger($fileUid)) {
-                        $fileObject = $fileFactory->getFileObject($fileUid);
-                        $fileLabel = $fileObject->getName();
-                    }
-                    $fileItem = $fileUid . '|' . $fileLabel;
-                }
-                // Showing thumbnails:
-                if ($show_thumbs) {
-                    foreach ($itemArray as $imgRead) {
-                        $imgP = explode('|', $imgRead);
-                        $imgPath = rawurldecode($imgP[0]);
-                        // FAL icon production
-                        if (MathUtility::canBeInterpretedAsInteger($imgP[0])) {
-                            $fileObject = $fileFactory->getFileObject($imgP[0]);
-                            if ($fileObject->isMissing()) {
-                                $thumbnails[] = [
-                                    'message' => '<span class="label label-danger">'
-                                        . htmlspecialchars(static::getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:warning.file_missing'))
-                                        . '</span>&nbsp;' . htmlspecialchars($fileObject->getName()) . '<br />'
-                                ];
-                            } elseif (GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $fileObject->getExtension())) {
-                                $thumbnails[] = [
-                                    'name' => htmlspecialchars($fileObject->getName()),
-                                    'image' => $fileObject->process(ProcessedFile::CONTEXT_IMAGEPREVIEW, [])->getPublicUrl(true)
-                                ];
-                            } else {
-                                $name = htmlspecialchars($fileObject->getName());
-                                // Icon
-                                $thumbnails[] = [
-                                    'name' => $name,
-                                    'image' => '<span title="' . $name . '">' . $this->iconFactory->getIconForResource($fileObject, Icon::SIZE_SMALL) . '</span>'
-                                ];
-                            }
-                        } else {
-                            $rowCopy = [];
-                            $rowCopy[$fieldName] = $imgPath;
-                            try {
-                                $thumbnails[] = [
-                                    'name' => $imgPath,
-                                    'image' => BackendUtility::thumbCode(
-                                        $rowCopy,
-                                        $table,
-                                        $fieldName,
-                                        '',
-                                        '',
-                                        $config['uploadfolder'],
-                                        0,
-                                        ' align="middle"'
-                                    )
-                                ];
-                            } catch (\Exception $exception) {
-                                /** @var $flashMessage FlashMessage */
-                                $message = $exception->getMessage();
-                                $flashMessage = GeneralUtility::makeInstance(
-                                    FlashMessage::class,
-                                    $message, '', FlashMessage::ERROR, true
-                                );
-                                /** @var $flashMessageService FlashMessageService */
-                                $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
-                                $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
-                                $defaultFlashMessageQueue->enqueue($flashMessage);
-                                $logMessage = $message . ' (' . $table . ':' . $row['uid'] . ')';
-                                GeneralUtility::sysLog($logMessage, 'core', GeneralUtility::SYSLOG_SEVERITY_WARNING);
+        $listOfSelectedValues = [];
+        $thumbnailsHtml = [];
+        $recordsOverviewHtml = [];
+        $selectorOptionsHtml = [];
+        $clipboardOnClick = [];
+        if ($internalType === 'file_reference' || $internalType === 'file') {
+            $fileFactory = ResourceFactory::getInstance();
+            foreach ($selectedItems as $selectedItem) {
+                $uidOrPath = $selectedItem['uidOrPath'];
+                $listOfSelectedValues[] = $uidOrPath;
+                $title = $selectedItem['title'];
+                $shortenedTitle = GeneralUtility::fixed_lgd_cs($title, $maxTitleLength);
+                $selectorOptionsHtml[] =
+                    '<option value="' . htmlspecialchars($uidOrPath) . '" title="' . htmlspecialchars($title) . '">'
+                        . htmlspecialchars($shortenedTitle)
+                    . '</option>';
+                if ($showThumbs) {
+                    if (MathUtility::canBeInterpretedAsInteger($uidOrPath)) {
+                        $fileObject = $fileFactory->getFileObject($uidOrPath);
+                        if (!$fileObject->isMissing()) {
+                            $extension = $fileObject->getExtension();
+                            if (GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'],
+                                $extension)
+                            ) {
+                                $thumbnailsHtml[] =
+                                    '<li>'
+                                        . '<span class="thumbnail">'
+                                            . $fileObject->process(ProcessedFile::CONTEXT_IMAGEPREVIEW, [])->getPublicUrl(true)
+                                        . '</span>'
+                                    . '</li>';
                             }
                         }
-                    }
-                }
-                // Creating the element:
-                $params = array_merge($commonParameters, [
-                    'allowed' => $allowed,
-                    'disallowed' => $disallowed,
-                    'thumbnails' => $thumbnails,
-                    'noDelete' => $noDelete
-                ]);
-                $html .= $this->dbFileIcons(
-                    $parameterArray['itemFormElName'],
-                    'file',
-                    implode(',', $allowed),
-                    $itemArray,
-                    '',
-                    $params,
-                    null,
-                    '',
-                    '',
-                    '',
-                    $config
-                );
-                if (!$disabled && !(isset($config['disable_controls']) && GeneralUtility::inList($config['disable_controls'], 'upload'))) {
-                    // Adding the upload field:
-                    $isDirectFileUploadEnabled = (bool)$this->getBackendUserAuthentication()->uc['edit_docModuleUpload'];
-                    if ($isDirectFileUploadEnabled && $config['uploadfolder']) {
-                        // Insert the multiple attribute to enable HTML5 multiple file upload
-                        $multipleAttribute = '';
-                        $multipleFilenameSuffix = '';
-                        if (isset($config['maxitems']) && $config['maxitems'] > 1) {
-                            $multipleAttribute = ' multiple="multiple"';
-                            $multipleFilenameSuffix = '[]';
+                    } else {
+                        $rowCopy = [];
+                        $rowCopy[$fieldName] = $uidOrPath;
+                        try {
+                            $icon = BackendUtility::thumbCode(
+                                $rowCopy,
+                                $table,
+                                $fieldName,
+                                '',
+                                '',
+                                $config['uploadfolder'],
+                                0,
+                                ' align="middle"'
+                            );
+                            $thumbnailsHtml[] =
+                                '<li>'
+                                    . '<span class="thumbnail">'
+                                        . $icon
+                                    . '</span>'
+                                . '</li>';
+                        } catch (\Exception $exception) {
+                            $message = $exception->getMessage();
+                            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, '',
+                                FlashMessage::ERROR, true);
+                            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
+                            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
+                            $defaultFlashMessageQueue->enqueue($flashMessage);
+                            $logMessage = $message . ' (' . $table . ':' . $row['uid'] . ')';
+                            GeneralUtility::sysLog($logMessage, 'core', GeneralUtility::SYSLOG_SEVERITY_WARNING);
                         }
-                        $html .= '
-                                                       <div id="' . $parameterArray['itemFormElID_file'] . '">
-                                                               <input type="file"' . $multipleAttribute . '
-                                                                       name="data_files' . $this->data['elementBaseName'] . $multipleFilenameSuffix . '"
-                                                                       size="35" onchange="' . implode('', $parameterArray['fieldChangeFunc']) . '"
-                                                               />
-                                                       </div>';
                     }
                 }
-                break;
-            case 'folder':
-                // If the element is of the internal type "folder":
-                // Array of folder items:
-                $itemArray = GeneralUtility::trimExplode(',', $parameterArray['itemFormElValue'], true);
-                // Creating the element:
-                $params = $commonParameters;
-                $html .= $this->dbFileIcons(
-                    $parameterArray['itemFormElName'],
-                    'folder',
-                    '',
-                    $itemArray,
-                    '',
-                    $params
-                );
-                break;
-            case 'db':
-                // If the element is of the internal type "db":
-                // Creating string showing allowed types:
-                $languageService = $this->getLanguageService();
+            }
+            foreach ($clipboardElements as $clipboardElement) {
+                $value = $clipboardElement['value'];
+                $title = 'unescape(' . GeneralUtility::quoteJSvalue(rawurlencode(basename($clipboardElement['title']))) . ')';
+                $clipboardOnClick[] = 'setFormValueFromBrowseWin('
+                        . GeneralUtility::quoteJSvalue($elementName) . ','
+                        . 'unescape(' . GeneralUtility::quoteJSvalue(rawurlencode(str_replace('%20', ' ', $value))) . '),'
+                        . $title . ','
+                        . $title
+                    . ');';
+            }
+        } elseif ($internalType === 'folder') {
+            foreach ($selectedItems as $selectedItem) {
+                $folder = $selectedItem['folder'];
+                $listOfSelectedValues[] = $folder;
+                $selectorOptionsHtml[] =
+                    '<option value="' . htmlspecialchars($folder) . '" title="' . htmlspecialchars($folder) . '">'
+                        . htmlspecialchars($folder)
+                    . '</option>';
+            }
+        } else {
+            // 'db'
+            foreach ($selectedItems as $selectedItem) {
+                $tableWithUid = $selectedItem['table'] . '_' . $selectedItem['uid'];
+                $listOfSelectedValues[] = $tableWithUid;
+                $title = $selectedItem['title'];
+                if (empty($title)) {
+                    $title = '[' . $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.no_title') . ']';
+                }
+                $shortenedTitle = GeneralUtility::fixed_lgd_cs($title, $maxTitleLength);
+                $selectorOptionsHtml[] =
+                    '<option value="' . htmlspecialchars($tableWithUid) . '" title="' . htmlspecialchars($title) . '">'
+                        . htmlspecialchars($shortenedTitle)
+                    . '</option>';
+                if (!$isDisabled && $showThumbs) {
+                    $linkedIcon = BackendUtility::wrapClickMenuOnIcon(
+                        $this->iconFactory->getIconForRecord($selectedItem['table'], $selectedItem['row'], Icon::SIZE_SMALL)->render(),
+                        $selectedItem['table'],
+                        $selectedItem['uid'],
+                        1,
+                        '',
+                        '+copy,info,edit,view'
+                    );
+                    $linkedTitle = BackendUtility::wrapClickMenuOnIcon(
+                        $shortenedTitle,
+                        $selectedItem['table'],
+                        $selectedItem['uid'],
+                        1,
+                        '',
+                        '+copy,info,edit,view'
+                    );
+                    $recordsOverviewHtml[] =
+                        '<tr>'
+                            . '<td class="col-icon">'
+                                . $linkedIcon
+                            . '</td>'
+                            . '<td class="col-title">'
+                                . $linkedTitle
+                                . '<span class="text-muted">'
+                                    . ' [' . $selectedItem['uid'] . ']'
+                                . '</span>'
+                            . '</td>'
+                        . '</tr>';
+                }
+            }
+            foreach ($clipboardElements as $clipboardElement) {
+                $value = $clipboardElement['value'];
+                $title = GeneralUtility::quoteJSvalue($clipboardElement['title']);
+                $clipboardOnClick[] = 'setFormValueFromBrowseWin('
+                    . GeneralUtility::quoteJSvalue($elementName) . ','
+                    . 'unescape(' . GeneralUtility::quoteJSvalue(rawurlencode(str_replace('%20', ' ', $value))) . '),'
+                    . $title . ','
+                    . $title
+                    . ');';
+            }
+        }
 
-                $allowedTables = [];
-                if ($allowed[0] === '*') {
-                    $allowedTables = [
-                        'name' => htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.allTables'))
-                    ];
-                } elseif ($allowed) {
-                    foreach ($allowed as $allowedTable) {
-                        $allowedTables[] = [
-                            // @todo: access to globals!
-                            'name' => htmlspecialchars($languageService->sL($GLOBALS['TCA'][$allowedTable]['ctrl']['title'])),
-                            'icon' => $this->iconFactory->getIconForRecord($allowedTable, [], Icon::SIZE_SMALL)->render(),
-                            'onClick' => 'setFormValueOpenBrowser(\'db\', ' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName'] . '|||' . $allowedTable) . '); return false;'
-                        ];
-                    }
+        // Check against inline uniqueness - Create some onclick js for delete control and element browser
+        // to override record selection in some FAL scenarios - See 'appearance' docs of group element
+        $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
+        $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
+        $elementBrowserOnClickInline = '';
+        $deleteControlOnClick = '';
+        if ($this->data['isInlineChild']
+            && $this->data['inlineParentUid']
+            && $this->data['inlineParentConfig']['foreign_table'] === $table
+            && $this->data['inlineParentConfig']['foreign_unique'] === $fieldName
+        ) {
+            $objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']) . '-' . $table;
+            $elementBrowserOnClickInline = $objectPrefix . '|inline.checkUniqueElement|inline.setUniqueElement';
+            $deleteControlOnClick = 'inline.revertUnique(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',null,' . GeneralUtility::quoteJSvalue($row['uid']) . ');';
+        }
+        $elementBrowserType = $internalType;
+        if (is_array($config['appearance']) && isset($config['appearance']['elementBrowserType'])) {
+            $elementBrowserType = $config['appearance']['elementBrowserType'];
+        }
+        $elementBrowserAllowed = implode(',', $allowed);
+        if (is_array($config['appearance']) && isset($config['appearance']['elementBrowserAllowed'])) {
+            $elementBrowserAllowed = $config['appearance']['elementBrowserAllowed'];
+        }
+        $elementBrowserOnClick = 'setFormValueOpenBrowser('
+                . GeneralUtility::quoteJSvalue($elementBrowserType) . ','
+                . GeneralUtility::quoteJSvalue($elementName . '|||' . $elementBrowserAllowed . '|' . $elementBrowserOnClickInline)
+            . ');'
+            . ' return false;';
+
+        $allowedTablesHtml = [];
+        if ($allowed[0] === '*') {
+            $allowedTablesHtml[] =
+                '<span>'
+                    . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.allTables'))
+                . '</span>';
+        } else {
+            foreach ($allowed as $tableName) {
+                $label = $languageService->sL($GLOBALS['TCA'][$tableName]['ctrl']['title']);
+                if (!$isDisabled) {
+                    $icon = $this->iconFactory->getIconForRecord($tableName, [], Icon::SIZE_SMALL);
+                    $onClick = 'setFormValueOpenBrowser(\'db\', ' . GeneralUtility::quoteJSvalue($elementName . '|||' . $tableName) . '); return false;';
+                    $allowedTablesHtml[] =
+                        '<a href="#" onClick="' . htmlspecialchars($onClick) . '" class="btn btn-default">'
+                            . $icon->render() . htmlspecialchars($label) . '</a> '
+                        . '</a>';
+                } else {
+                    $allowedTablesHtml[] = '<span>' . htmlspecialchars($label) . '</span> ';
                 }
-                $perms_clause = $this->getBackendUserAuthentication()->getPagePermsClause(1);
-                $itemArray = [];
+            }
+        }
 
-                // Thumbnails:
-                // @todo: this is data processing - must be extracted
-                $temp_itemArray = GeneralUtility::trimExplode(',', $parameterArray['itemFormElValue'], true);
-                foreach ($temp_itemArray as $dbRead) {
-                    $recordParts = explode('|', $dbRead);
-                    list($this_table, $this_uid) = BackendUtility::splitTable_Uid($recordParts[0]);
+        $allowedHtml = [];
+        foreach ($allowed as $item) {
+            $allowedHtml[] = '<span class="label label-success">' . htmlspecialchars(strtoupper($item)) . '</span> ';
+        }
 
-                    $itemArray[] = ['table' => $this_table, 'id' => $this_uid];
-                    if (!$disabled && $show_thumbs) {
-                        if (empty($this_table)) {
-                            throw new \RuntimeException(
-                                'Table name could not been determined for field "' . $fieldName . '" in table "' . $table . '". ' .
-                                'This should never happen since the table name should have been already prepared in the DataProvider TcaGroup. ' .
-                                'Maybe the prepared values have been set to an invalid value by a user defined data provider.',
-                                1468149217
-                            );
-                        }
-                        $rr = BackendUtility::getRecordWSOL($this_table, $this_uid);
-                        $thumbnails[] = [
-                            'name' => BackendUtility::getRecordTitle($this_table, $rr, true),
-                            'image' => $this->iconFactory->getIconForRecord($this_table, $rr, Icon::SIZE_SMALL)->render(),
-                            'path' => BackendUtility::getRecordPath($rr['pid'], $perms_clause, 15),
-                            'uid' => $rr['uid'],
-                            'table' => $this_table
-                        ];
-                    }
+        $disallowedHtml = [];
+        foreach ($disallowed as $item) {
+            $disallowedHtml[] = '<span class="label label-danger">' . htmlspecialchars(strtoupper($item)) . '</span> ';
+        }
+
+        $selectorStyles = [];
+        $selectorAttributes = [];
+        $selectorAttributes[] = 'id="' . StringUtility::getUniqueId('tceforms-multiselect-') . '"';
+        $selectorAttributes[] = 'data-formengine-input-name="' . htmlspecialchars($elementName) . '"';
+        $selectorAttributes[] = $this->getValidationDataAsDataAttribute($config);
+        if ($maxItems !== 1 && $size !== 1) {
+            $selectorAttributes[] = 'multiple="multiple"';
+        }
+        if ($isDisabled) {
+            $selectorAttributes[] = 'disabled="disabled"';
+        }
+        if ($showListControl) {
+            $selectorClasses = [];
+            $selectorClasses[] = 'form-control';
+            $selectorClasses[] = 'tceforms-multiselect';
+            if ($maxItems === 1) {
+                $selectorClasses[] = 'form-select-no-siblings';
+            }
+            $selectorAttributes[] = 'class="' . implode(' ', $selectorClasses) . '"';
+            $selectorAttributes[] = 'size="' . $size . '"';
+        } else {
+            $selectorStyles[] = 'display: none';
+        }
+        if (isset($config['selectedListStyle'])) {
+            $selectorStyles[] = $config['selectedListStyle'];
+        }
+        $selectorAttributes[] = 'style="' . implode(';', $selectorStyles) . '"';
+
+        $html = [];
+        $html[] = '<input type="hidden" class="t3js-group-hidden-field" data-formengine-input-name="' . htmlspecialchars($elementName) . '" value="' . $itemCanBeSelectedMoreThanOnce . '" />';
+        $html[] = '<div class="form-wizards-wrap form-wizards-aside">';
+        $html[] =   '<div class="form-wizards-element">';
+        $html[] =       '<select ' . implode(' ', $selectorAttributes) . '>';
+        $html[] =           implode(LF, $selectorOptionsHtml);
+        $html[] =       '</select>';
+        if ($showListControl && $showAllowedTables && $internalType === 'db' && !empty($allowedTablesHtml)) {
+            $html[] =       '<div class="help-block">';
+            $html[] =           implode(LF, $allowedTablesHtml);
+            $html[] =       '</div>';
+        }
+        if ($showListControl && $internalType === 'file' && (!empty($allowedHtml) || !empty($disallowedHtml)) && !$isDisabled) {
+            $html[] =       '<div class="help-block">';
+            $html[] =           implode(LF, $allowedHtml);
+            $html[] =           implode(LF, $disallowedHtml);
+            $html[] =       '</div>';
+        }
+        $html[] =   '</div>';
+
+        $html[] =   '<div class="form-wizards-items">';
+        $html[] =       '<div class="btn-group-vertical">';
+        if ($maxItems > 1 && $size >=5 && $selectedItemsCount >=5 && !$isDisabled && $showMoveIcons) {
+            $html[] =       '<a href="#"';
+            $html[] =           ' class="btn btn-default t3js-btn-moveoption-top"';
+            $html[] =           ' data-fieldname="' . htmlspecialchars($elementName) . '"';
+            $html[] =           ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move_to_top')) . '"';
+            $html[] =       '>';
+            $html[] =           $this->iconFactory->getIcon('actions-move-to-top', Icon::SIZE_SMALL)->render();
+            $html[] =       '</a>';
+        }
+        if ($maxItems > 1 && !$isDisabled && $showMoveIcons) {
+            $html[] =       '<a href="#"';
+            $html[] =           ' class="btn btn-default t3js-btn-moveoption-up"';
+            $html[] =           ' data-fieldname="' . htmlspecialchars($elementName) . '"';
+            $html[] =           ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move_up')) . '"';
+            $html[] =       '>';
+            $html[] =           $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render();
+            $html[] =       '</a>';
+            $html[] =       '<a href="#"';
+            $html[] =           ' class="btn btn-default t3js-btn-moveoption-down"';
+            $html[] =           ' data-fieldname="' . htmlspecialchars($elementName) . '"';
+            $html[] =           ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move_down')) . '"';
+            $html[] =       '>';
+            $html[] =           $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render();
+            $html[] =       '</a>';
+        }
+        if ($maxItems > 1 && $size >=5 && $selectedItemsCount >=5 && !$isDisabled && $showMoveIcons) {
+            $html[] =       '<a href="#"';
+            $html[] =           ' class="btn btn-default t3js-btn-moveoption-bottom"';
+            $html[] =           ' data-fieldname="' . htmlspecialchars($elementName) . '"';
+            $html[] =           ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move_to_bottom')) . '"';
+            $html[] =       '>';
+            $html[] =           $this->iconFactory->getIcon('actions-move-to-bottom', Icon::SIZE_SMALL)->render();
+            $html[] =       '</a>';
+        }
+        if ($showDeleteControl && !$isDisabled) {
+            $html[] =       '<a href="#"';
+            $html[] =           ' class="btn btn-default t3js-btn-removeoption"';
+            $html[] =           ' data-fieldname="' . htmlspecialchars($elementName) . '"';
+            $html[] =           ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.remove_selected')) . '"';
+            $html[] =           ' onClick="' . $deleteControlOnClick . '"';
+            $html[] =       '>';
+            $html[] =           $this->iconFactory->getIcon('actions-selection-delete', Icon::SIZE_SMALL)->render();
+            $html[] =       '</a>';
+        }
+        $html[] =       '</div>';
+        $html[] =   '</div>';
+
+        $html[] =   '<div class="form-wizards-items">';
+        $html[] =       '<div class="btn-group-vertical">';
+        if ($showListControl && $showBrowseControl && !$isDisabled) {
+            if ($internalType === 'db') {
+                $elementBrowserLabel = $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.browse_db');
+            } else {
+                $elementBrowserLabel = $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.browse_file');
+            }
+            $html[] =       '<a href="#"';
+            $html[] =           ' onclick="' . htmlspecialchars($elementBrowserOnClick) . '"';
+            $html[] =           ' class="btn btn-default"';
+            $html[] =           ' title="' . htmlspecialchars($elementBrowserLabel) . '"';
+            $html[] =       '>';
+            $html[] =           $this->iconFactory->getIcon('actions-insert-record', Icon::SIZE_SMALL)->render();
+            $html[] =       '</a>';
+        }
+        if ($showListControl && $showBrowseControl && !$isDisabled && !empty($clipboardElements)) {
+            if ($internalType === 'db') {
+                $clipboardLabel = sprintf($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.clipInsert_db'), count($clipboardElements));
+            } else {
+                $clipboardLabel = sprintf($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.clipInsert_file'), count($clipboardElements));
+            }
+            $html[] =       '<a href="#"';
+            $html[] =           ' onclick="' . htmlspecialchars(implode(LF, $clipboardOnClick)) . ' return false;"';
+            $html[] =           ' class="btn btn-default"';
+            $html[] =           ' title="' . htmlspecialchars($clipboardLabel) . '"';
+            $html[] =       '>';
+            $html[] =           $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render();
+            $html[] =       '</a>';
+        }
+        $html[] =       '</div>';
+        $html[] =   '</div>';
+        $html[] = '</div>';
+
+        if (!empty($thumbnailsHtml)) {
+            $html[] = '<ul class="list-inline">';
+            $html[] =   implode(LF, $thumbnailsHtml);
+            $html[] = '</ul>';
+        }
+        if (!empty($recordsOverviewHtml)) {
+            $html[] = '<div class="table-fit">';
+            $html[] =   '<table class="table table-white">';
+            $html[] =       '<tbody>';
+            $html[] =           implode(LF, $recordsOverviewHtml);
+            $html[] =       '</tbody>';
+            $html[] =   '</table>';
+            $html[] = '</div>';
+        }
+
+        if (!$isDisabled && $showUploadField) {
+            // Adding the upload field
+            if ($isDirectFileUploadEnabled && !empty($config['uploadfolder'])) {
+                // Insert the multiple attribute to enable HTML5 multiple file upload
+                $selectorMultipleAttribute = '';
+                $multipleFilenameSuffix = '';
+                if ($maxItems > 1) {
+                    $selectorMultipleAttribute = ' multiple="multiple"';
+                    $multipleFilenameSuffix = '[]';
                 }
-                // Creating the element:
-                $params = array_merge($commonParameters, [
-                    'info' => $info,
-                    'allowedTables' => $allowedTables,
-                    'thumbnails' => $thumbnails,
-                ]);
-                $html .= $this->dbFileIcons(
-                    $parameterArray['itemFormElName'],
-                    'db',
-                    implode(',', $allowed),
-                    $itemArray,
-                    '',
-                    $params,
-                    null,
-                    $table,
-                    $fieldName,
-                    $row['uid'],
-                    $config
-                );
-                break;
-        }
-        // Wizards:
-        if (!$disabled) {
+                $html[] = '<div id="' . $uploadFieldId . '">';
+                $html[] =   '<input';
+                $html[] =       ' type="file"';
+                $html[] =       $selectorMultipleAttribute;
+                $html[] =       ' name="data_files' . $this->data['elementBaseName'] . $multipleFilenameSuffix . '"';
+                $html[] =       ' size="35"';
+                $html[] =       ' onchange="' . implode('', $parameterArray['fieldChangeFunc']) . '"';
+                $html[] =   '/>';
+                $html[] = '</div>';
+            }
+        }
+
+        $html[] = '<input type="hidden" name="' . htmlspecialchars($elementName) . '" value="' . htmlspecialchars(implode(',', $listOfSelectedValues)) . '" />';
+
+        $html = implode(LF, $html);
+
+        if (!$config['readOnly']) {
             $html = $this->renderWizards(
-                [$html],
+                [ $html ],
                 $config['wizards'],
                 $table,
                 $row,
                 $fieldName,
                 $parameterArray,
-                $parameterArray['itemFormElName'],
-                $specConf
+                $elementName,
+                BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras'])
             );
         }
+
         $resultArray['html'] = $html;
         return $resultArray;
     }
index 3f4efed..f69794a 100644 (file)
@@ -23,7 +23,8 @@ use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
 
 /**
- * Generation of image manipulation TCEform element
+ * Generation of image manipulation FormEngine element.
+ * This is typically used in FAL relations to cut images.
  */
 class ImageManipulationElement extends AbstractFormElement
 {
@@ -168,14 +169,8 @@ class ImageManipulationElement extends AbstractFormElement
     {
         $file = null;
         $fileUid = !empty($row[$fieldName]) ? $row[$fieldName] : null;
-        if (strpos($fileUid, 'sys_file_') === 0) {
-            if (strpos($fileUid, '|')) {
-                // @todo: uid_local is a group field that was resolved to table_uid|target - split here again
-                // @todo: this will vanish if group fields are moved to array
-                $fileUid = explode('|', $fileUid);
-                $fileUid = $fileUid[0];
-            }
-            $fileUid = substr($fileUid, 9);
+        if (is_array($fileUid) && isset($fileUid[0]['uid'])) {
+            $fileUid = $fileUid[0]['uid'];
         }
         if (MathUtility::canBeInterpretedAsInteger($fileUid)) {
             try {
index 109b371..2bb6895 100644 (file)
@@ -138,7 +138,7 @@ class InputColorPickerElement extends AbstractFormElement
                 if (class_exists($evalData)) {
                     $evalObj = GeneralUtility::makeInstance($evalData);
                     if (method_exists($evalObj, 'returnFieldJS')) {
-                        $resultArray['extJSCODE'] .= LF . 'TBE_EDITOR.customEvalFunctions[' . GeneralUtility::quoteJSvalue($evalData) . '] = function(value) {' . $evalObj->returnFieldJS() . '}';
+                        $resultArray['additionalJavaScriptPost'][] = 'TBE_EDITOR.customEvalFunctions[' . GeneralUtility::quoteJSvalue($evalData) . '] = function(value) {' . $evalObj->returnFieldJS() . '};';
                     }
                 }
             }
index 029f68e..bbaeae2 100644 (file)
@@ -179,7 +179,7 @@ class InputTextElement extends AbstractFormElement
                 if (class_exists($evalData)) {
                     $evalObj = GeneralUtility::makeInstance($evalData);
                     if (method_exists($evalObj, 'returnFieldJS')) {
-                        $resultArray['extJSCODE'] .= LF . 'TBE_EDITOR.customEvalFunctions[' . GeneralUtility::quoteJSvalue($evalData) . '] = function(value) {' . $evalObj->returnFieldJS() . '}';
+                        $resultArray['additionalJavaScriptPost'][] = 'TBE_EDITOR.customEvalFunctions[' . GeneralUtility::quoteJSvalue($evalData) . '] = function(value) {' . $evalObj->returnFieldJS() . '};';
                     }
                 }
             }
index d4ce283..43b9efc 100644 (file)
@@ -23,7 +23,7 @@ use TYPO3\CMS\Core\Utility\StringUtility;
 /**
  * Creates a widget with check box elements.
  *
- * This is rendered for config type=select, renderType=selectCheckBox, maxitems > 1
+ * This is rendered for config type=select, renderType=selectCheckBox
  */
 class SelectCheckBoxElement extends AbstractFormElement
 {
index c236999..dfe6089 100644 (file)
@@ -16,13 +16,15 @@ namespace TYPO3\CMS\Backend\Form\Element;
 
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
+use TYPO3\CMS\Lang\LanguageService;
 
 /**
  * Render a widget with two boxes side by side.
  *
- * This is rendered for config type=select, maxitems > 1, renderType=selectMultipleSideBySide set
+ * This is rendered for config type=select, renderType=selectMultipleSideBySide set
  */
 class SelectMultipleSideBySideElement extends AbstractFormElement
 {
@@ -33,160 +35,226 @@ class SelectMultipleSideBySideElement extends AbstractFormElement
      */
     public function render()
     {
-        $table = $this->data['tableName'];
-        $field = $this->data['fieldName'];
+        $languageService = $this->getLanguageService();
+
         $parameterArray = $this->data['parameterArray'];
-        // Field configuration from TCA:
         $config = $parameterArray['fieldConf']['config'];
+        $elementName = $parameterArray['itemFormElName'];
 
-        $selItems = $config['items'];
-        $html = '';
-        $disabled = '';
         if ($config['readOnly']) {
-            $disabled = ' disabled="disabled"';
-        }
-        // Setting this hidden field (as a flag that JavaScript can read out)
-        if (!$disabled) {
-            $html .= '<input type="hidden" data-formengine-input-name="' . htmlspecialchars($parameterArray['itemFormElName']) . '" value="' . ($config['multiple'] ? 1 : 0) . '" />';
+            // Early return for the relatively simple read only case
+            return $this->renderReadOnly();
         }
-        // Set max and min items:
-        $maxitems = MathUtility::forceIntegerInRange($config['maxitems'], 0);
-        if (!$maxitems) {
-            $maxitems = 100000;
-        }
-        // Get the array with selected items:
-        $itemsArray = $parameterArray['itemFormElValue'] ?: [];
 
-        // Perform modification of the selected items array:
-        foreach ($itemsArray as $itemNumber => $itemValue) {
-            $itemArray = [
-                0 => $itemValue,
-                1 => '',
-            ];
+        $possibleItems = $config['items'];
+        $selectedItems = $parameterArray['itemFormElValue'] ?: [];
+        $selectedItemsCount = count($selectedItems);
 
-            if (isset($parameterArray['fieldTSConfig']['altIcons.'][$itemValue])) {
-                $itemArray[2] = $parameterArray['fieldTSConfig']['altIcons.'][$itemValue];
-            }
+        $maxItems = $config['maxitems'];
+        $autoSizeMax = MathUtility::forceIntegerInRange($config['autoSizeMax'], 0);
+        $size = 2;
+        if (isset($config['size'])) {
+            $size = (int)$config['size'];
+        }
+        if ($autoSizeMax >= 1) {
+            $size = MathUtility::forceIntegerInRange($selectedItemsCount + 1, MathUtility::forceIntegerInRange($size, 1), $autoSizeMax);
+        }
+        $itemCanBeSelectedMoreThanOnce = !empty($config['multiple']);
 
-            foreach ($selItems as $selItem) {
-                if ($selItem[1] == $itemValue) {
-                    $itemArray[1] = $selItem[0];
+        $listOfSelectedValues = [];
+        $selectedItemsHtml = [];
+        foreach ($selectedItems as $itemValue) {
+            foreach ($possibleItems as $possibleItem) {
+                if ($possibleItem[1] == $itemValue) {
+                    $title = $possibleItem[0];
+                    $listOfSelectedValues[] = $itemValue;
+                    $selectedItemsHtml[] = '<option value="' . htmlspecialchars($itemValue) . '" title="' . htmlspecialchars($title) . '">' . htmlspecialchars($title) . '</option>';
                     break;
                 }
             }
-            $itemsArray[$itemNumber] = implode('|', $itemArray);
         }
 
-        // size must be at least two, as there are always maxitems > 1 (see parent function)
-        if (isset($config['size'])) {
-            $size = (int)$config['size'];
-        } else {
-            $size = 2;
+        $selectableItemsHtml = [];
+        foreach ($possibleItems as $possibleItem) {
+            $disabledAttr = '';
+            $classAttr = '';
+            if (!$itemCanBeSelectedMoreThanOnce && in_array((string)$possibleItem[1], $selectedItems, true)) {
+                $disabledAttr = ' disabled="disabled"';
+                $classAttr = ' class="hidden"';
+            }
+            $selectableItemsHtml[] =
+                '<option value="'
+                    . htmlspecialchars($possibleItem[1])
+                    . '" title="' . htmlspecialchars($possibleItem[0]) . '"'
+                    . $classAttr . $disabledAttr
+                . '>'
+                    . htmlspecialchars($possibleItem[0]) .
+                '</option>';
         }
-        $size = $config['autoSizeMax'] ? MathUtility::forceIntegerInRange(count($itemsArray) + 1, MathUtility::forceIntegerInRange($size, 1), $config['autoSizeMax']) : $size;
-        $allowMultiple = !empty($config['multiple']);
 
-        $itemsToSelect = [];
+        // Html stuff for filter and select filter on top of right side of multi select boxes
         $filterTextfield = [];
-        $filterSelectbox = '';
-        if (!$disabled) {
-            // Create option tags:
-            $opt = [];
-            foreach ($selItems as $p) {
-                $disabledAttr = '';
-                $classAttr = '';
-                if (!$allowMultiple && in_array((string)$p[1], $parameterArray['itemFormElValue'], true)) {
-                    $disabledAttr = ' disabled="disabled"';
-                    $classAttr = ' class="hidden"';
+        if ($config['enableMultiSelectFilterTextfield']) {
+            $filterTextfield[] = '<span class="input-group input-group-sm">';
+            $filterTextfield[] =    '<span class="input-group-addon">';
+            $filterTextfield[] =        '<span class="fa fa-filter"></span>';
+            $filterTextfield[] =    '</span>';
+            $filterTextfield[] =    '<input class="t3js-formengine-multiselect-filter-textfield form-control" value="">';
+            $filterTextfield[] = '</span>';
+        }
+        $filterDropDownOptions = [];
+        if (isset($config['multiSelectFilterItems']) && is_array($config['multiSelectFilterItems']) && count($config['multiSelectFilterItems']) > 1) {
+            foreach ($config['multiSelectFilterItems'] as $optionElement) {
+                $value = $languageService->sL($optionElement[0]);
+                $label = $value;
+                if (isset($optionElement[1]) && trim($optionElement[1]) !== '') {
+                    $label = $languageService->sL($optionElement[1]);
                 }
-                $opt[] = '<option value="' . htmlspecialchars($p[1]) . '" title="' . htmlspecialchars($p[0]) . '"' . $classAttr . $disabledAttr . '>' . htmlspecialchars($p[0]) . '</option>';
+                $filterDropDownOptions[] = '<option value="' . htmlspecialchars($value) . '">' . htmlspecialchars($label) . '</option>';
             }
-            // Put together the selector box:
-            $selector_itemListStyle = isset($config['itemListStyle'])
-                ? ' style="' . htmlspecialchars($config['itemListStyle']) . '"'
-                : '';
-            $sOnChange = implode('', $parameterArray['fieldChangeFunc']);
-
-            $multiSelectId = StringUtility::getUniqueId('tceforms-multiselect-');
-            $itemsToSelect[] = '<select data-relatedfieldname="' . htmlspecialchars($parameterArray['itemFormElName']) . '" '
-                . 'data-exclusivevalues="' . htmlspecialchars($config['exclusiveKeys']) . '" '
-                . 'id="' . $multiSelectId . '" '
-                . 'data-formengine-input-name="' . htmlspecialchars($parameterArray['itemFormElName']) . '" '
-                . 'class="form-control t3js-formengine-select-itemstoselect" '
-                . ($size > 1 ? ' size="' . $size . '" ' : '')
-                . 'onchange="' . htmlspecialchars($sOnChange) . '" '
-                . $this->getValidationDataAsDataAttribute($config)
-                . $selector_itemListStyle
-                . '>';
-            $itemsToSelect[] = implode(LF, $opt);
-            $itemsToSelect[] = '</select>';
-
-            // enable filter functionality via a text field
-            if ($config['enableMultiSelectFilterTextfield']) {
-                $filterTextfield[] = '<span class="input-group input-group-sm">';
-                $filterTextfield[] =    '<span class="input-group-addon">';
-                $filterTextfield[] =        '<span class="fa fa-filter"></span>';
-                $filterTextfield[] =    '</span>';
-                $filterTextfield[] =    '<input class="t3js-formengine-multiselect-filter-textfield form-control" value="">';
-                $filterTextfield[] = '</span>';
+        }
+        $filterHtml = [];
+        if (!empty($filterTextfield) || !empty($filterDropDownOptions)) {
+            $filterHtml[] = '<div class="form-multigroup-item-wizard">';
+            if (!empty($filterTextfield) && !empty($filterDropDownOptions)) {
+                $filterHtml[] = '<div class="t3js-formengine-multiselect-filter-container form-multigroup-wrap">';
+                $filterHtml[] =     '<div class="form-multigroup-item form-multigroup-element">';
+                $filterHtml[] =         '<select class="form-control input-sm t3js-formengine-multiselect-filter-dropdown">';
+                $filterHtml[] =             implode(LF, $filterDropDownOptions);
+                $filterHtml[] =         '</select>';
+                $filterHtml[] =     '</div>';
+                $filterHtml[] =     '<div class="form-multigroup-item form-multigroup-element">';
+                $filterHtml[] =         implode(LF, $filterTextfield);
+                $filterHtml[] =     '</div>';
+                $filterHtml[] = '</div>';
+            } elseif (!empty($filterTextfield)) {
+                $filterHtml[] = implode(LF, $filterTextfield);
+            } else {
+                $filterHtml[] = '<select class="form-control input-sm t3js-formengine-multiselect-filter-dropdown">';
+                $filterHtml[] =     implode(LF, $filterDropDownOptions);
+                $filterHtml[] = '</select>';
             }
+            $filterHtml[] = '</div>';
+        }
 
-            // enable filter functionality via a select
-            if (isset($config['multiSelectFilterItems']) && is_array($config['multiSelectFilterItems']) && count($config['multiSelectFilterItems']) > 1) {
-                $filterDropDownOptions = [];
-                foreach ($config['multiSelectFilterItems'] as $optionElement) {
-                    $optionValue = $this->getLanguageService()->sL(isset($optionElement[1]) && trim($optionElement[1]) !== '' ? trim($optionElement[1])
-                        : trim($optionElement[0]));
-                    $filterDropDownOptions[] = '<option value="' . htmlspecialchars($this->getLanguageService()->sL(trim($optionElement[0]))) . '">'
-                        . htmlspecialchars($optionValue) . '</option>';
-                }
-                $filterSelectbox = '<select class="form-control input-sm t3js-formengine-multiselect-filter-dropdown">'
-                    . implode(LF, $filterDropDownOptions) . '</select>';
-            }
+        $classes = [];
+        $classes[] = 'form-control';
+        $classes[] = 'tceforms-multiselect';
+        if ($maxItems === 1) {
+            $classes[] = 'form-select-no-siblings';
+        }
+        $multipleAttribute = '';
+        if ($maxItems !== 1 && $size !== 1) {
+            $multipleAttribute = ' multiple="multiple"';
+        }
+        $selectedListStyle = '';
+        if (isset($config['selectedListStyle'])) {
+            $selectedListStyle = ' style="' . htmlspecialchars($config['selectedListStyle']) . '"';
+        }
+        $selectableListStyle = '';
+        if (isset($config['itemListStyle'])) {
+            $selectableListStyle = ' style="' . htmlspecialchars($config['itemListStyle']) . '"';
         }
 
-        if (!empty(trim($filterSelectbox)) && !empty($filterTextfield)) {
-            $filterSelectbox = '<div class="form-multigroup-item form-multigroup-element">' . $filterSelectbox . '</div>';
-            $filterTextfield = '<div class="form-multigroup-item form-multigroup-element">' . implode(LF, $filterTextfield) . '</div>';
-            $selectBoxFilterContents = '<div class="t3js-formengine-multiselect-filter-container form-multigroup-wrap">' . $filterSelectbox . $filterTextfield . '</div>';
-        } else {
-            $selectBoxFilterContents = trim($filterSelectbox . ' ' . implode(LF, $filterTextfield));
-        }
-
-        // Pass to "dbFileIcons" function:
-        $params = [
-            'size' => $size,
-            'autoSizeMax' => MathUtility::forceIntegerInRange($config['autoSizeMax'], 0),
-            'style' => isset($config['selectedListStyle'])
-                ? ' style="' . htmlspecialchars($config['selectedListStyle']) . '"'
-                : '',
-            'dontShowMoveIcons' => $maxitems <= 1,
-            'maxitems' => $maxitems,
-            'info' => '',
-            'headers' => [
-                'selector' => $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.selected'),
-                'items' => $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.items'),
-                'selectorbox' => $selectBoxFilterContents,
-            ],
-            'noBrowser' => 1,
-            'rightbox' => implode(LF, $itemsToSelect),
-            'readOnly' => $disabled
-        ];
-        $html .= $this->dbFileIcons($parameterArray['itemFormElName'], '', '', $itemsArray, '', $params);
-
-        // Wizards:
-        if (!$disabled) {
-            $html = $this->renderWizards(
-                [$html],
-                $config['wizards'],
-                $table,
-                $this->data['databaseRow'],
-                $field,
-                $parameterArray,
-                $parameterArray['itemFormElName'],
-                BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras'])
-            );
+        $html = [];
+        $html[] = '<input type="hidden" data-formengine-input-name="' . htmlspecialchars($elementName) . '" value="' . (int)$itemCanBeSelectedMoreThanOnce . '" />';
+        $html[] = '<div class="form-multigroup-wrap t3js-formengine-field-group">';
+        $html[] =   '<div class="form-multigroup-item form-multigroup-element">';
+        $html[] =       '<label>';
+        $html[] =           htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.selected'));
+        $html[] =       '</label>';
+        $html[] =       '<div class="form-wizards-wrap form-wizards-aside">';
+        $html[] =           '<div class="form-wizards-element">';
+        $html[] =               '<select';
+        $html[] =                   ' id="' . StringUtility::getUniqueId('tceforms-multiselect-') . '"';
+        $html[] =                   ' size="' . $size . '"';
+        $html[] =                   ' class="' . implode(' ', $classes) . '"';
+        $html[] =                   $multipleAttribute;
+        $html[] =                   ' data-formengine-input-name="' . htmlspecialchars($elementName) . '"';
+        $html[] =                   $selectedListStyle;
+        $html[] =               '>';
+        $html[] =                   implode(LF, $selectedItemsHtml);
+        $html[] =               '</select>';
+        $html[] =           '</div>';
+        $html[] =           '<div class="form-wizards-items">';
+        $html[] =               '<div class="btn-group-vertical">';
+        if ($maxItems > 1 && $size >= 5) {
+            $html[] =               '<a href="#"';
+            $html[] =                   ' class="btn btn-default t3js-btn-moveoption-top"';
+            $html[] =                   ' data-fieldname="' . htmlspecialchars($elementName) . '"';
+            $html[] =                   ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move_to_top')) . '"';
+            $html[] =               '>';
+            $html[] =                   $this->iconFactory->getIcon('actions-move-to-top', Icon::SIZE_SMALL)->render();
+            $html[] =               '</a>';
+        }
+        if ($maxItems > 1) {
+            $html[] =               '<a href="#"';
+            $html[] =                   ' class="btn btn-default t3js-btn-moveoption-up"';
+            $html[] =                   ' data-fieldname="' . htmlspecialchars($elementName) . '"';
+            $html[] =                   ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move_up')) . '"';
+            $html[] =               '>';
+            $html[] =                   $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render();
+            $html[] =               '</a>';
+            $html[] =               '<a href="#"';
+            $html[] =                   ' class="btn btn-default t3js-btn-moveoption-down"';
+            $html[] =                   ' data-fieldname="' . htmlspecialchars($elementName) . '"';
+            $html[] =                   ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move_down')) . '"';
+            $html[] =               '>';
+            $html[] =                   $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render();
+            $html[] =               '</a>';
+        }
+        if ($maxItems > 1 && $size >= 5) {
+            $html[] =               '<a href="#"';
+            $html[] =                   ' class="btn btn-default t3js-btn-moveoption-bottom"';
+            $html[] =                   ' data-fieldname="' . htmlspecialchars($elementName) . '"';
+            $html[] =                   ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.move_to_bottom')) . '"';
+            $html[] =               '>';
+            $html[] =                   $this->iconFactory->getIcon('actions-move-to-bottom', Icon::SIZE_SMALL)->render();
+            $html[] =               '</a>';
         }
+        $html[] =                   '<a href="#"';
+        $html[] =                       ' class="btn btn-default t3js-btn-removeoption"';
+        $html[] =                       ' data-fieldname="' . htmlspecialchars($elementName) . '"';
+        $html[] =                       ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.remove_selected')) . '"';
+        $html[] =                   '>';
+        $html[] =                       $this->iconFactory->getIcon('actions-selection-delete', Icon::SIZE_SMALL)->render();
+        $html[] =                   '</a>';
+        $html[] =               '</div>';
+        $html[] =           '</div>';
+        $html[] =       '</div>';
+        $html[] =   '</div>';
+        $html[] =   '<div class="form-multigroup-item form-multigroup-element">';
+        $html[] =       '<label>';
+        $html[] =           htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.items'));
+        $html[] =       '</label>';
+        $html[] =       implode(LF, $filterHtml);
+        $html[] =       '<select';
+        $html[] =           ' data-relatedfieldname="' . htmlspecialchars($elementName) . '"';
+        $html[] =           ' data-exclusivevalues="' . htmlspecialchars($config['exclusiveKeys']) . '"';
+        $html[] =           ' id="' . StringUtility::getUniqueId('tceforms-multiselect-') . '"';
+        $html[] =           ' data-formengine-input-name="' . htmlspecialchars($elementName) . '"';
+        $html[] =           ' class="form-control t3js-formengine-select-itemstoselect"';
+        $html[] =           ' size="' . $size . '"';
+        $html[] =           ' onchange="' . htmlspecialchars(implode('', $parameterArray['fieldChangeFunc'])) . '"';
+        $html[] =           $this->getValidationDataAsDataAttribute($config);
+        $html[] =           $selectableListStyle;
+        $html[] =       '>';
+        $html[] =           implode(LF, $selectableItemsHtml);
+        $html[] =       '</select>';
+        $html[] =   '</div>';
+        $html[] = '</div>';
+        $html[] = '<input type="hidden" name="' . htmlspecialchars($elementName) . '" value="' . htmlspecialchars(implode(',', $listOfSelectedValues)) . '" />';
+
+        $html = $this->renderWizards(
+            [ implode(LF, $html) ],
+            $config['wizards'],
+            $this->data['tableName'],
+            $this->data['databaseRow'],
+            $this->data['fieldName'],
+            $parameterArray,
+            $elementName,
+            BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras'])
+        );
 
         $resultArray = $this->initializeResultArray();
         $resultArray['html'] = $html;
@@ -194,6 +262,88 @@ class SelectMultipleSideBySideElement extends AbstractFormElement
     }
 
     /**
+     * Create HTML of a read only multi select. Right side is not
+     * rendered, but just the left side with the selected items.
+     *
+     * @return array
+     */
+    protected function renderReadOnly()
+    {
+        $languageService = $this->getLanguageService();
+
+        $parameterArray = $this->data['parameterArray'];
+        $config = $parameterArray['fieldConf']['config'];
+        $fieldName = $parameterArray['itemFormElName'];
+
+        $possibleItems = $config['items'];
+        $selectedItems = $parameterArray['itemFormElValue'] ?: [];
+        $selectedItemsCount = count($selectedItems);
+
+        $autoSizeMax = MathUtility::forceIntegerInRange($config['autoSizeMax'], 0);
+        $size = 2;
+        if (isset($config['size'])) {
+            $size = (int)$config['size'];
+        }
+        if ($autoSizeMax >= 1) {
+            $size = MathUtility::forceIntegerInRange($selectedItemsCount + 1, MathUtility::forceIntegerInRange($size, 1), $autoSizeMax);
+        }
+        $multiple = '';
+        if ($size !== 1) {
+            $multiple = ' multiple="multiple"';
+        }
+        $style = '';
+        if (isset($config['selectedListStyle'])) {
+            $style = ' style="' . htmlspecialchars($config['selectedListStyle']) . '"';
+        }
+
+        $listOfSelectedValues = [];
+        $optionsHtml = [];
+        foreach ($selectedItems as $itemValue) {
+            foreach ($possibleItems as $possibleItem) {
+                if ($possibleItem[1] == $itemValue) {
+                    $title = $possibleItem[0];
+                    $listOfSelectedValues[] = $itemValue;
+                    $optionsHtml[] = '<option value="' . htmlspecialchars($itemValue) . '" title="' . htmlspecialchars($title) . '">' . htmlspecialchars($title) . '</option>';
+                    break;
+                }
+            }
+        }
+
+        $html = [];
+        $html[] = '<label>';
+        $html[] =   htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.selected'));
+        $html[] = '</label>';
+        $html[] = '<div class="form-wizards-wrap form-wizards-aside">';
+        $html[] =   '<div class="form-wizards-element">';
+        $html[] =       '<select';
+        $html[] =           ' id="' . StringUtility::getUniqueId('tceforms-multiselect-') . '"';
+        $html[] =           ' size="' . $size . '"';
+        $html[] =           ' class="form-control tceforms-multiselect"';
+        $html[] =           $multiple;
+        $html[] =           ' data-formengine-input-name="' . htmlspecialchars($fieldName) . '"';
+        $html[] =           $style;
+        $html[] =           ' disabled="disabled">';
+        $html[] =       '/>';
+        $html[] =           implode(LF, $optionsHtml);
+        $html[] =       '</select>';
+        $html[] =   '</div>';
+        $html[] = '</div>';
+        $html[] = '<input type="hidden" name="' . htmlspecialchars($fieldName) . '" value="' . htmlspecialchars(implode(',', $listOfSelectedValues)) . '" />';
+
+        $resultArray = $this->initializeResultArray();
+        $resultArray['html'] = implode(LF, $html);
+        return $resultArray;
+    }
+
+    /**
+     * @return LanguageService
+     */
+    protected function getLanguageService()
+    {
+        return $GLOBALS['LANG'];
+    }
+
+    /**
      * @return BackendUserAuthentication
      */
     protected function getBackendUserAuthentication()
index e0d60ed..5d0400b 100644 (file)
@@ -23,7 +23,7 @@ use TYPO3\CMS\Core\Utility\StringUtility;
 /**
  * Create a widget with a select box where multiple items can be selected
  *
- * This is rendered for config type=select, maxitems > 1, renderType=selectSingleBox
+ * This is rendered for config type=select, renderType=selectSingleBox
  */
 class SelectSingleBoxElement extends AbstractFormElement
 {
index feb749c..caf9250 100644 (file)
@@ -24,7 +24,7 @@ use TYPO3\CMS\Core\Utility\StringUtility;
  * Creates a widget where only one item can be selected.
  * This is either a select drop-down if no size config is given or set to 1, or a select box.
  *
- * This is rendered for type=select, maxitems=1
+ * This is rendered for type=select, renderType=selectSingle
  */
 class SelectSingleElement extends AbstractFormElement
 {
index 4af7e77..807c55b 100644 (file)
@@ -14,10 +14,6 @@ namespace TYPO3\CMS\Backend\Form\Element;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
-use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-
 /**
  * Render data as a tree.
  *
@@ -77,22 +73,54 @@ class SelectTreeElement extends AbstractFormElement
         $treeWrapperId = 'tree_' . $formElementId;
 
         $fieldName = $this->data['fieldName'];
-        $flexDataStructureIdentifier = '';
+
+        $dataStructureIdentifier = '';
+        $flexFormSheetName = '';
+        $flexFormFieldName = '';
+        $flexFormContainerName = '';
+        $flexFormContainerIdentifier = '';
+        $flexFormContainerFieldName = '';
+        $flexFormSectionContainerIsNew = false;
         if ($this->data['processedTca']['columns'][$fieldName]['config']['type'] === 'flex') {
-            $flexDataStructureIdentifier = $this->data['processedTca']['columns'][$fieldName]['config']['dataStructureIdentifier'];
+            $dataStructureIdentifier = $this->data['processedTca']['columns'][$fieldName]['config']['dataStructureIdentifier'];
+            if (isset($this->data['flexFormSheetName'])) {
+                $flexFormSheetName = $this->data['flexFormSheetName'];
+            }
+            if (isset($this->data['flexFormFieldName'])) {
+                $flexFormFieldName = $this->data['flexFormFieldName'];
+            }
+            if (isset($this->data['flexFormContainerName'])) {
+                $flexFormContainerName = $this->data['flexFormContainerName'];
+            }
+            if (isset($this->data['flexFormContainerFieldName'])) {
+                $flexFormContainerFieldName = $this->data['flexFormContainerFieldName'];
+            }
+            if (isset($this->data['flexFormContainerIdentifier'])) {
+                $flexFormContainerIdentifier = $this->data['flexFormContainerIdentifier'];
+            }
+            // Add a flag this is a tree in a new flex section container element. This is needed to initialize
+            // the databaseRow with this container again so the tree data provider is able to calculate tree items.
+            if (!empty($this->data['flexSectionContainerPreparation'])) {
+                $flexFormSectionContainerIsNew = true;
+            }
         }
 
         $html = [];
         $html[] = '<div class="typo3-tceforms-tree">';
         $html[] = '    <input class="treeRecord" type="hidden"';
         $html[] = '           ' . $this->getValidationDataAsDataAttribute($config);
-        $html[] = '           data-formengine-input-name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
         $html[] = '           data-relatedfieldname="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
-        $html[] = '           data-table="' . htmlspecialchars($this->data['tableName']) . '"';
-        $html[] = '           data-field="' . htmlspecialchars($this->data['fieldName']) . '"';
-        $html[] = '           data-flex-form-datastructure-identifier="' . htmlspecialchars($flexDataStructureIdentifier) . '"';
+        $html[] = '           data-tablename="' . htmlspecialchars($this->data['tableName']) . '"';
+        $html[] = '           data-fieldname="' . htmlspecialchars($this->data['fieldName']) . '"';
         $html[] = '           data-uid="' . (int)$this->data['vanillaUid'] . '"';
-        $html[] = '           data-recordtypevalue="' . $this->data['recordTypeValue'] . '"';
+        $html[] = '           data-recordtypevalue="' . htmlspecialchars($this->data['recordTypeValue']) . '"';
+        $html[] = '           data-datastructureidentifier="' . htmlspecialchars($dataStructureIdentifier) . '"';
+        $html[] = '           data-flexformsheetname="' . htmlspecialchars($flexFormSheetName) . '"';
+        $html[] = '           data-flexformfieldname="' . htmlspecialchars($flexFormFieldName) . '"';
+        $html[] = '           data-flexformcontainername="' . htmlspecialchars($flexFormContainerName) . '"';
+        $html[] = '           data-flexformcontaineridentifier="' . htmlspecialchars($flexFormContainerIdentifier) . '"';
+        $html[] = '           data-flexformcontainerfieldname="' . htmlspecialchars($flexFormContainerFieldName) . '"';
+        $html[] = '           data-flexformsectioncontainerisnew="' . htmlspecialchars($flexFormSectionContainerIsNew) . '"';
         $html[] = '           data-command="' . htmlspecialchars($this->data['command']) . '"';
         $html[] = '           data-read-only="' . $readOnly . '"';
         $html[] = '           data-tree-exclusive-keys="' . htmlspecialchars($exclusiveKeys) . '"';
@@ -126,34 +154,9 @@ class SelectTreeElement extends AbstractFormElement
      */
     protected function getTreeOnChangeJs()
     {
-        $table = $this->data['tableName'];
-        $field = $this->data['fieldName'];
         $parameterArray = $this->data['parameterArray'];
         $onChange = !empty($parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged']) ? $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] : '';
         $onChange .= !empty($parameterArray['fieldChangeFunc']['alert']) ? $parameterArray['fieldChangeFunc']['alert'] : '';
-
-        // 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($GLOBALS['TCA'][$table]['ctrl']['type'])
-            && $field === $GLOBALS['TCA'][$table]['ctrl']['type']
-            || !empty($GLOBALS['TCA'][$table]['ctrl']['requestUpdate'])
-            && GeneralUtility::inList(str_replace(' ', '', $GLOBALS['TCA'][$table]['ctrl']['requestUpdate']), $field)
-        ) {
-            if ($this->getBackendUserAuthentication()->jsConfirmation(JsConfirmation::TYPE_CHANGE)) {
-                $onChange = 'top.TYPO3.Modal.confirm(TYPO3.lang["FormEngine.refreshRequiredTitle"], TYPO3.lang["FormEngine.refreshRequiredContent"]).on("button.clicked", function(e) { if (e.target.name == "ok" && TBE_EDITOR.checkSubmit(-1)) { TBE_EDITOR.submitForm() } top.TYPO3.Modal.dismiss(); });';
-            } else {
-                $onChange .= 'if (TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm() };';
-            }
-        }
         return 'function () {' . $onChange . '}';
     }
-
-    /**
-     * @return BackendUserAuthentication
-     */
-    protected function getBackendUserAuthentication()
-    {
-        return $GLOBALS['BE_USER'];
-    }
 }
index ff8729c..553c396 100644 (file)
@@ -203,9 +203,14 @@ class FormDataCompiler
             // of the record this flex form is embedded in is transferred in case features like single fields
             // itemsProcFunc need to have this data at hand to do their job.
             'flexParentDatabaseRow' => [],
+            // If not empty, it tells the TcaFlexProcess data provider to not calculate existing flex fields and
+            // existing flex container sections, but to instead prepare field values and the data structure TCA
+            // for a new container section. This is used by FormFlexAjaxController, the array contains details
+            // which container of which flex field should be created.
+            'flexSectionContainerPreparation' => [],
 
             // If true, TcaSelectTreeItems data provider will compile tree items. This is false by default since
-            // on opening a record items are not calculated but are fetch in an ajax request, see SelectTreeController.
+            // on opening a record items are not calculated but are fetch in an ajax request, see FormSelectTreeAjaxController.
             'selectTreeCompileItems' => false,
 
             // BackendUser->uc['inlineView'] - This array holds status of expand / collapsed inline items
@@ -270,7 +275,6 @@ class FormDataCompiler
 
             // @todo: keys below must be handled / further defined
             'elementBaseName' => '',
-            'flexFormFieldIdentifierPrefix' => 'ID',
             'tabAndInlineStack' => [],
             'inlineData' => [],
             'inlineStructure' => [],
index f388e6f..1a7d671 100644 (file)
@@ -1170,7 +1170,10 @@ abstract class AbstractItemProvider
         $currentDatabaseValues = array_key_exists($fieldName, $row)
             ? $row[$fieldName]
             : '';
-        return GeneralUtility::trimExplode(',', $currentDatabaseValues, true);
+        if (!is_array($currentDatabaseValues)) {
+            $currentDatabaseValues = GeneralUtility::trimExplode(',', $currentDatabaseValues, true);
+        }
+        return $currentDatabaseValues;
     }
 
     /**
@@ -1309,15 +1312,17 @@ abstract class AbstractItemProvider
      *
      * @param mixed $maxItems
      * @return int
+     * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
      */
     public function sanitizeMaxItems($maxItems)
     {
+        GeneralUtility::logDeprecatedFunction();
         if (!empty($maxItems)
-            && (int)$maxItems > 1
+            && (int)$maxItems >= 1
         ) {
             $maxItems = (int)$maxItems;
         } else {
-            $maxItems = 1;
+            $maxItems = 99999;
         }
 
         return $maxItems;
index dc8a85d..f614d6f 100644 (file)
@@ -98,11 +98,9 @@ class DatabaseRecordTypeValue implements FormDataProviderInterface
                             1438253614
                         );
                     }
-                    // Extract UID from value formed like {table_name}_{uid}|{default_value}
-                    // @todo: This needs adaption as soon as the group format is changed
-                    if (!MathUtility::canBeInterpretedAsInteger($foreignUid)) {
-                        list($foreignUid) = explode('|', $foreignUid);
-                        $foreignUid = str_replace($foreignTable . '_', '', $foreignUid);
+                    if (!MathUtility::canBeInterpretedAsInteger($foreignUid) && is_array($foreignUid[0])) {
+                        // A group relation - has been resolved to array by TcaGroup data provider already
+                        $foreignUid = $foreignUid[0]['uid'];
                     }
                     // Fetch field of this foreign row from db
                     $foreignRow = $this->getDatabaseRow($foreignTable, $foreignUid, $foreignTableTypeField);
@@ -152,6 +150,7 @@ class DatabaseRecordTypeValue implements FormDataProviderInterface
 
         return $row ?: [];
     }
+
     /**
      * If a localized row is handled, the field value of the default language record
      * is used instead if tca is configured as "exclude" or "mergeIfNotBlank" with
index 4946c53..cfc9404 100644 (file)
@@ -37,7 +37,7 @@ class DatabaseRowDefaultValues implements FormDataProviderInterface
 
         $newRow = $databaseRow;
         foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
-            // Keep current value if it can be resolved to "the is something" directly
+            // Keep current value if it can be resolved to "there is something" directly
             if (isset($databaseRow[$fieldName])) {
                 $newRow[$fieldName] = $databaseRow[$fieldName];
                 continue;
@@ -55,7 +55,9 @@ class DatabaseRowDefaultValues implements FormDataProviderInterface
                     $newRow[$fieldName] = (string)$fieldConfig['config']['default'];
                 }
             } else {
-                // Fun part: This forces empty string for any field even if no default is set. Unsure if that is a good idea.
+                // Fun part: This forces empty string for any field even if no default is set. This is
+                // a useful side effect in flex form section containers where a new field is added to an existing
+                // value array because it was added to a data structure.
                 $newRow[$fieldName] = (string)$fieldConfig['config']['default'];
             }
         }
index ce12433..8dd5bcb 100644 (file)
@@ -34,14 +34,22 @@ class DatabaseUniqueUidNewRow implements FormDataProviderInterface
         if ($result['command'] !== 'new') {
             return $result;
         }
-        // Throw exception if uid is already set
-        if (isset($result['databaseRow']['uid'])) {
+        // Throw exception if uid is already set and does not start with NEW.
+        // In some situations a new record needs to be created again so the initialization of default
+        // values is triggered, but the "ID" of the new record is already known: This is the case if a
+        // new section container element is added by FormFlexAjaxController to a not yet persisted record.
+        // In this case, command "new" is given to the data compiler, but the "NEW1234" id has been calculated
+        // by the former compiler when opening the record already. The ajax controller then hands in the
+        // "new" command together with the id calculated by the first call.
+        if (isset($result['databaseRow']['uid']) && strpos($result['databaseRow']['uid'], 'NEW') !== 0) {
             throw new \InvalidArgumentException(
-                'uid is already set to ' . $result['databaseRow']['uid'],
+                'uid is already set to ' . $result['databaseRow']['uid'] . ' and does not start with NEW for a "new" command',
                 1437991120
             );
         }
-        $result['databaseRow']['uid'] = StringUtility::getUniqueId('NEW');
+        if (!isset($result['databaseRow']['uid'])) {
+            $result['databaseRow']['uid'] = StringUtility::getUniqueId('NEW');
+        }
 
         return $result;
     }
index 8c30e23..5544c45 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+declare(strict_types=1);
 namespace TYPO3\CMS\Backend\Form\FormDataProvider;
 
 /*
@@ -16,6 +17,7 @@ namespace TYPO3\CMS\Backend\Form\FormDataProvider;
 
 use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MathUtility;
 
 /**
  * Class EvaluateDisplayConditions implements the TCA 'displayCond' option.
@@ -27,295 +29,759 @@ class EvaluateDisplayConditions implements FormDataProviderInterface
     /**
      * Remove fields from processedTca columns that should not be displayed.
      *
+     * Strategy of the parser is to first find all displayCond in given tca
+     * and within all type=flex fields to parse them into an array. This condition
+     * array contains all information to evaluate that condition in a second
+     * step that - depending on evaluation result - then throws away or keeps the field.
+     *
      * @param array $result
      * @return array
      */
-    public function addData(array $result)
+    public function addData(array $result): array
     {
-        $result = $this->removeFlexformFields($result);
-        $result = $this->removeFlexformSheets($result);
-        $result = $this->removeTcaColumns($result);
-
+        $result = $this->parseDisplayConditions($result);
+        $result = $this->evaluateConditions($result);
         return $result;
     }
 
     /**
-     * Evaluate the TCA column display conditions and remove columns that are not displayed
+     * Find all 'displayCond' in TCA and flex forms and substitute them with an
+     * array representation that contains all relevant data to
+     * evaluate the condition later. For "FIELD" conditions the helper methods
+     * findFieldValue() is used to find the value of the referenced field to put
+     * that value into the returned array, too. This is important since the referenced
+     * field is "relative" to the position of the field that has the display condition.
+     * For instance, "FIELD:aField:=:foo" within a flex form field references a field
+     * value from the same sheet, and there are many more complex scenarios to resolve.
      *
-     * @param array $result
-     * @return array
+     * @param array $result Incoming result array
+     * @throws \RuntimeException
+     * @return array Modified result array with all displayCond parsed into arrays
      */
-    protected function removeTcaColumns($result)
+    protected function parseDisplayConditions(array $result): array
     {
+        $flexColumns = [];
         foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) {
+            if ($columnConfiguration['config']['type'] === 'flex') {
+                $flexColumns[$columnName] = $columnConfiguration;
+            }
             if (!isset($columnConfiguration['displayCond'])) {
                 continue;
             }
+            $result['processedTca']['columns'][$columnName]['displayCond'] = $this->parseConditionRecursive(
+                $columnConfiguration['displayCond'],
+                $result['databaseRow']
+            );
+        }
 
-            if (!$this->evaluateDisplayCondition($columnConfiguration['displayCond'], $result['databaseRow'])) {
-                unset($result['processedTca']['columns'][$columnName]);
+        foreach ($flexColumns as $columnName => $flexColumn) {
+            $sheetNameFieldNames = [];
+            foreach ($flexColumn['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) {
+                // Create a list of all sheet names with field names combinations for later 'sheetName.fieldName' lookups
+                // 'one.sheet.one.field' as key, with array of "sheetName" and "fieldName" as value
+                if (isset($sheetConfiguration['ROOT']['el']) && is_array($sheetConfiguration['ROOT']['el'])) {
+                    foreach ($sheetConfiguration['ROOT']['el'] as $flexElementName => $flexElementConfiguration) {
+                        // section container have no value in its own
+                        if (isset($flexElementConfiguration['type']) && $flexElementConfiguration['type'] === 'array'
+                            && isset($flexElementConfiguration['section']) && $flexElementConfiguration['section'] == 1
+                        ) {
+                            continue;
+                        }
+                        $combinedKey = $sheetName . '.' . $flexElementName;
+                        if (array_key_exists($combinedKey, $sheetNameFieldNames)) {
+                            throw new \RuntimeException(
+                                'Ambiguous sheet name and field name combination: Sheet "' . $sheetNameFieldNames[$combinedKey]['sheetName']
+                                . '" with field name "' . $sheetNameFieldNames[$combinedKey]['fieldName'] . '" overlaps with sheet "'
+                                . $sheetName . '" and field name "' . $flexElementName . '". Do not do that.',
+                                1481483061
+                            );
+                        }
+                        $sheetNameFieldNames[$combinedKey] = [
+                            'sheetName' => $sheetName,
+                            'fieldName' => $flexElementName,
+                        ];
+                    }
+                }
+            }
+            foreach ($flexColumn['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) {
+                if (isset($sheetConfiguration['ROOT']['displayCond'])) {
+                    // Condition on a flex sheet
+                    $flexContext = [
+                        'context' => 'flexSheet',
+                        'sheetNameFieldNames' => $sheetNameFieldNames,
+                        'currentSheetName' => $sheetName,
+                        'flexFormRowData' => $result['databaseRow'][$columnName],
+                    ];
+                    $parsedDisplayCondition = $this->parseConditionRecursive(
+                        $sheetConfiguration['ROOT']['displayCond'],
+                        $result['databaseRow'],
+                        $flexContext
+                    );
+                    $result['processedTca']['columns'][$columnName]['config']['ds']
+                        ['sheets'][$sheetName]['ROOT']['displayCond']
+                        = $parsedDisplayCondition;
+                }
+                if (isset($sheetConfiguration['ROOT']['el']) && is_array($sheetConfiguration['ROOT']['el'])) {
+                    foreach ($sheetConfiguration['ROOT']['el'] as $flexElementName => $flexElementConfiguration) {
+                        if (isset($flexElementConfiguration['displayCond'])) {
+                            // Condition on a flex element
+                            $flexContext = [
+                                'context' => 'flexField',
+                                'sheetNameFieldNames' => $sheetNameFieldNames,
+                                'currentSheetName' => $sheetName,
+                                'currentFieldName' => $flexElementName,
+                                'flexFormDataStructure' => $result['processedTca']['columns'][$columnName]['config']['ds'],
+                                'flexFormRowData' => $result['databaseRow'][$columnName],
+                            ];
+                            $parsedDisplayCondition = $this->parseConditionRecursive(
+                                $flexElementConfiguration['displayCond'],
+                                $result['databaseRow'],
+                                $flexContext
+                            );
+                            $result['processedTca']['columns'][$columnName]['config']['ds']
+                                ['sheets'][$sheetName]['ROOT']
+                                ['el'][$flexElementName]['displayCond']
+                                = $parsedDisplayCondition;
+                        }
+                        if (isset($flexElementConfiguration['type']) && $flexElementConfiguration['type'] === 'array'
+                            && isset($flexElementConfiguration['section']) && $flexElementConfiguration['section'] == 1
+                            && isset($flexElementConfiguration['children']) && is_array($flexElementConfiguration['children'])
+                        ) {
+                            // Conditions on flex container section elements
+                            foreach ($flexElementConfiguration['children'] as $containerIdentifier => $containerElements) {
+                                if (isset($containerElements['el']) && is_array($containerElements['el'])) {
+                                    foreach ($containerElements['el'] as $containerElementName => $containerElementConfiguration) {
+                                        if (isset($containerElementConfiguration['displayCond'])) {
+                                            $flexContext = [
+                                                'context' => 'flexContainerElement',
+                                                'sheetNameFieldNames' => $sheetNameFieldNames,
+                                                'currentSheetName' => $sheetName,
+                                                'currentFieldName' => $flexElementName,
+                                                'currentContainerIdentifier' => $containerIdentifier,
+                                                'currentContainerElementName' => $containerElementName,
+                                                'flexFormDataStructure' => $result['processedTca']['columns'][$columnName]['config']['ds'],
+                                                'flexFormRowData' => $result['databaseRow'][$columnName],
+                                            ];
+                                            $parsedDisplayCondition = $this->parseConditionRecursive(
+                                                $containerElementConfiguration['displayCond'],
+                                                $result['databaseRow'],
+                                                $flexContext
+                                            );
+                                            $result['processedTca']['columns'][$columnName]['config']['ds']
+                                                ['sheets'][$sheetName]['ROOT']
+                                                ['el'][$flexElementName]
+                                                ['children'][$containerIdentifier]
+                                                ['el'][$containerElementName]['displayCond']
+                                                = $parsedDisplayCondition;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
             }
         }
-
         return $result;
     }
 
     /**
-     * Remove flexform sheets from processed tca if hidden by display conditions
+     * Parse a condition into an array representation and validate syntax. Handles nested conditions combined with AND and OR.
+     * Calls itself recursive for nesting and logically combined conditions.
      *
-     * @param array $result
-     * @return array
+     * @param mixed $condition Either an array with multiple conditions combined with AND or OR, or a single condition string
+     * @param array $databaseRow Incoming full database row
+     * @param array $flexContext Detailed flex context if display condition is within a flex field, needed to determine field value for "FIELD" conditions
+     * @throws \RuntimeException
+     * @return array Array representation of that condition, see unit tests for details on syntax
      */
-    protected function removeFlexformSheets($result)
+    protected function parseConditionRecursive($condition, array $databaseRow, array $flexContext = []): array
     {
-        foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) {
-            if (!isset($columnConfiguration['config']['type'])
-                || $columnConfiguration['config']['type'] !== 'flex'
-                || !isset($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'])
-                || !is_array($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'])
-            ) {
-                continue;
-            }
-
-            $flexFormRowData = is_array($result['databaseRow'][$columnName]['data']) ? $result['databaseRow'][$columnName]['data'] : [];
-            $flexFormRowData = $this->flattenFlexformRowData($flexFormRowData);
-            $flexFormRowData['parentRec'] = $result['databaseRow'];
-
-            $flexFormSheets = $result['processedTca']['columns'][$columnName]['config']['ds']['sheets'];
-            foreach ($flexFormSheets as $sheetName => $sheetConfiguration) {
-                if (!isset($sheetConfiguration['ROOT']['displayCond'])) {
-                    continue;
+        $conditionArray = [];
+        if (is_string($condition)) {
+            $conditionArray = $this->parseSingleConditionString($condition, $databaseRow, $flexContext);
+        } elseif (is_array($condition)) {
+            foreach ($condition as $logicalOperator => $groupedDisplayConditions) {
+                $logicalOperator = strtoupper($logicalOperator);
+                if (($logicalOperator !== 'AND' && $logicalOperator !== 'OR') || !is_array($groupedDisplayConditions)) {
+                    throw new \RuntimeException(
+                        'Multiple conditions must have boolean operator "OR" or "AND", "' . $logicalOperator . '" given.',
+                        1481380393
+                    );
+                }
+                if (count($groupedDisplayConditions) < 2) {
+                    throw new \RuntimeException(
+                        'With multiple conditions combined by "' . $logicalOperator . '", there must be at least two sub conditions',
+                        1481464101
+                    );
                 }
-                if (!$this->evaluateDisplayCondition($sheetConfiguration['ROOT']['displayCond'], $flexFormRowData, true)) {
-                    unset($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'][$sheetName]);
+                $conditionArray = [
+                    'type' => $logicalOperator,
+                    'subConditions' => [],
+                ];
+                foreach ($groupedDisplayConditions as $key => $singleDisplayCondition) {
+                    $key = strtoupper((string)$key);
+                    if (($key === 'AND' || $key === 'OR') && is_array($singleDisplayCondition)) {
+                        // Recursion statement: condition is 'AND' or 'OR' and is pointing to an array (should be conditions again)
+                        $conditionArray['subConditions'][] = $this->parseConditionRecursive(
+                            [$key => $singleDisplayCondition],
+                            $databaseRow,
+                            $flexContext
+                        );
+                    } else {
+                        $conditionArray['subConditions'][] = $this->parseConditionRecursive(
+                            $singleDisplayCondition,
+                            $databaseRow,
+                            $flexContext
+                        );
+                    }
                 }
             }
+        } else {
+            throw new \RuntimeException(
+                'Condition must be either an array with sub conditions or a single condition string, type ' . gettype($condition) . ' given.',
+                1481381058
+            );
         }
-
-        return $result;
+        return $conditionArray;
     }
 
     /**
-     * Remove fields from flexform sheets if hidden by display conditions
+     * Parse a single condition string into pieces, validate them and return
+     * an array representation.
      *
-     * @param array $result
-     * @return array
+     * @param string $conditionString Given condition string like "VERSION:IS:true"
+     * @param array $databaseRow Incoming full database row
+     * @param array $flexContext Detailed flex context if display condition is within a flex field, needed to determine field value for "FIELD" conditions
+     * @return array Validated name array, example: [ type="VERSION", isVersion="true" ]
+     * @throws \RuntimeException
      */
-    protected function removeFlexformFields($result)
+    protected function parseSingleConditionString(string $conditionString, array $databaseRow, array $flexContext = []): array
     {
-        foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) {
-            if (!isset($columnConfiguration['config']['type'])
-                || $columnConfiguration['config']['type'] !== 'flex'
-                || !isset($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'])
-                || !is_array($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'])
-            ) {
-                continue;
-            }
-
-            $flexFormRowData = is_array($result['databaseRow'][$columnName]['data']) ? $result['databaseRow'][$columnName]['data'] : [];
-            $flexFormRowData['parentRec'] = $result['databaseRow'];
-
-            foreach ($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) {
-                $flexFormSheetRowData = $flexFormRowData[$sheetName]['lDEF'];
-                $flexFormSheetRowData['parentRec'] = $result['databaseRow'];
-                $result['processedTca']['columns'][$columnName]['config']['ds']['sheets'][$sheetName] = $this->removeFlexformFieldsRecursive(
-                    $result['processedTca']['columns'][$columnName]['config']['ds']['sheets'][$sheetName],
-                    $flexFormSheetRowData
+        $conditionArray = GeneralUtility::trimExplode(':', $conditionString);
+        $namedConditionArray = [
+            'type' => $conditionArray[0],
+        ];
+        switch ($namedConditionArray['type']) {
+            case 'FIELD':
+                if (empty($conditionArray[1])) {
+                    throw new \RuntimeException(
+                        'Field condition "' . $conditionString . '" must have a field name as second part, none given.'
+                        . 'Example: "FIELD:myField:=:myValue"',
+                        1481385695
+                    );
+                }
+                $fieldName = $conditionArray[1];
+                $allowedOperators = [ 'REQ', '>', '<', '>=', '<=', '-', '!-', '=', '!=', 'IN', '!IN', 'BIT', '!BIT' ];
+                if (empty($conditionArray[2]) || !in_array($conditionArray[2], $allowedOperators)) {
+                    throw new \RuntimeException(
+                        'Field condition "' . $conditionString . '" must have a valid operator as third part, non or invalid one given.'
+                        . ' Valid operators are: "' . implode('", "', $allowedOperators) . '".'
+                        . ' Example: "FIELD:myField:=:4"',
+                        1481386239
+                    );
+                }
+                $namedConditionArray['operator'] = $conditionArray[2];
+                if (!isset($conditionArray[3])) {
+                    throw new \RuntimeException(
+                        'Field condition "' . $conditionString . '" must have an operand as fourth part, none given.'
+                        . ' Example: "FIELD:myField:=:4"',
+                        1481401543
+                    );
+                }
+                $operand = $conditionArray[3];
+                if ($namedConditionArray['operator'] === 'REQ') {
+                    $operand = strtolower($operand);
+                    if ($operand === 'true') {
+                        $namedConditionArray['operand'] = true;
+                    } elseif ($operand === 'false') {
+                        $namedConditionArray['operand'] = false;
+                    } else {
+                        throw new \RuntimeException(
+                            'Field condition "' . $conditionString . '" must have "true" or "false" as fourth part.'
+                            . ' Example: "FIELD:myField:REQ:true',
+                            1481401892
+                        );
+                    }
+                } elseif (in_array($namedConditionArray['operator'], [ '>', '<', '>=', '<=', 'BIT', '!BIT' ])) {
+                    if (!MathUtility::canBeInterpretedAsInteger($operand)) {
+                        throw new \RuntimeException(
+                            'Field condition "' . $conditionString . '" with comparison operator ' . $namedConditionArray['operator']
+                            . ' must have a number as fourth part, ' . $operand . ' given. Example: "FIELD:myField:>:42"',
+                            1481456806
+                        );
+                    }
+                    $namedConditionArray['operand'] = (int)$operand;
+                } elseif ($namedConditionArray['operator'] === '-' || $namedConditionArray['operator'] === '!-') {
+                    list($minimum, $maximum) = GeneralUtility::trimExplode('-', $operand);
+                    if (!MathUtility::canBeInterpretedAsInteger($minimum) || !MathUtility::canBeInterpretedAsInteger($maximum)) {
+                        throw new \RuntimeException(
+                            'Field condition "' . $conditionString . '" with comparison operator ' . $namedConditionArray['operator']
+                            . ' must have two numbers as fourth part, separated by dash, ' . $operand . ' given. Example: "FIELD:myField:-:1-3"',
+                            1481457277
+                        );
+                    }
+                    $namedConditionArray['operand'] = '';
+                    $namedConditionArray['min'] = (int)$minimum;
+                    $namedConditionArray['max'] = (int)$maximum;
+                } elseif ($namedConditionArray['operator'] === 'IN' || $namedConditionArray['operator'] === '!IN'
+                    || $namedConditionArray['operator'] === '=' || $namedConditionArray['operator'] === '!='
+                ) {
+                    $namedConditionArray['operand'] = $operand;
+                }
+                $namedConditionArray['fieldValue'] = $this->findFieldValue($fieldName, $databaseRow, $flexContext);
+                break;
+            case 'HIDE_FOR_NON_ADMINS':
+                break;
+            case 'REC':
+                if (empty($conditionArray[1]) || $conditionArray[1] !== 'NEW') {
+                    throw new \RuntimeException(
+                        'Record condition "' . $conditionString . '" must contain "NEW" keyword: either "REC:NEW:true" or "REC:NEW:false"',
+                        1481384784
+                    );
+                }
+                if (empty($conditionArray[2])) {
+                    throw new \RuntimeException(
+                        'Record condition "' . $conditionString . '" must have an operand "true" or "false", none given. Example: "REC:NEW:true"',
+                        1481384947
+                    );
+                }
+                $operand = strtolower($conditionArray[2]);
+                if ($operand === 'true') {
+                    $namedConditionArray['isNew'] = true;
+                } elseif ($operand === 'false') {
+                    $namedConditionArray['isNew'] = false;
+                } else {
+                    throw new \RuntimeException(
+                        'Record condition "' . $conditionString . '" must have an operand "true" or "false, example "REC:NEW:true", given: ' . $operand,
+                        1481385173
+                    );
+                }
+                // Programming error: There must be a uid available, other data providers should have taken care of that already
+                if (!array_key_exists('uid', $databaseRow)) {
+                    throw new \RuntimeException(
+                        'Required [\'databaseRow\'][\'uid\'] not found in data array',
+                        1481467208
+                    );
+                }
+                // May contain "NEW123..."
+                $namedConditionArray['uid'] = $databaseRow['uid'];
+                break;
+            case 'VERSION':
+                if (empty($conditionArray[1]) || $conditionArray[1] !== 'IS') {
+                    throw new \RuntimeException(
+                        'Version condition "' . $conditionString . '" must contain "IS" keyword: either "VERSION:IS:false" or "VERSION:IS:true"',
+                        1481383660
+                    );
+                }
+                if (empty($conditionArray[2])) {
+                    throw new \RuntimeException(
+                        'Version condition "' . $conditionString . '" must have an operand "true" or "false", none given. Example: "VERSION:IS:true',
+                        1481383888
+                    );
+                }
+                $operand = strtolower($conditionArray[2]);
+                if ($operand === 'true') {
+                    $namedConditionArray['isVersion'] = true;
+                } elseif ($operand === 'false') {
+                    $namedConditionArray['isVersion'] = false;
+                } else {
+                    throw new \RuntimeException(
+                        'Version condition "' . $conditionString . '" must have a "true" or "false" operand, example "VERSION:IS:true", given: ' . $operand,
+                        1481384123
+                    );
+                }
+                // Programming error: There must be a uid available, other data providers should have taken care of that already
+                if (!array_key_exists('uid', $databaseRow)) {
+                    throw new \RuntimeException(
+                        'Required [\'databaseRow\'][\'uid\'] not found in data array',
+                        1481469854
+                    );
+                }
+                $namedConditionArray['uid'] = $databaseRow['uid'];
+                if (array_key_exists('pid', $databaseRow)) {
+                    $namedConditionArray['pid'] = $databaseRow['pid'];
+                }
+                if (array_key_exists('_ORIG_pid', $databaseRow)) {
+                    $namedConditionArray['_ORIG_pid'] = $databaseRow['_ORIG_pid'];
+                }
+                break;
+            case 'USER':
+                if (empty($conditionArray[1])) {
+                    throw new \RuntimeException(
+                        'User function condition "' . $conditionString . '" must have a user function defined a second part, none given.'
+                        . ' Correct format is USER:\My\User\Func->match:more:arguments,'
+                        . ' given: ' . $conditionString,
+                        1481382954
+                    );
+                }
+                $namedConditionArray['function'] = $namedConditionArray[1];
+                array_shift($namedConditionArray);
+                array_shift($namedConditionArray);
+                $namedConditionArray['parameters'] = $namedConditionArray;
+                $namedConditionArray['record'] = $databaseRow;
+                break;
+            default:
+                throw new \RuntimeException(
+                    'Unknown condition rule type "' . $namedConditionArray['type'] . '" with display condition "' . $conditionString . '"".',
+                    1481381950
                 );
-            }
         }
-
-        return $result;
+        return $namedConditionArray;
     }
 
     /**
-     * Remove fields from flexform data structure
+     * Find field value the condition refers to for "FIELD:" conditions.  For "normal" TCA fields this is the value of
+     * a "neighbor" field, but in flex form context it can be prepended with a sheet name. The method sorts out the
+     * details and returns the current field value.
      *
-     * @param array $structure Given hierarchy
-     * @param array $flexFormRowData
-     * @return array Modified hierarchy
+     * @param string $givenFieldName The full name used in displayCond. Can have sheet names included in flex context
+     * @param array $databaseRow Incoming database row values
+     * @param array $flexContext Detailed flex context if display condition is within a flex field, needed to determine field value for "FIELD" conditions
+     * @throws \RuntimeException
+     * @return mixed The current field value from database row or a deeper flex form structure field.
      */
-    protected function removeFlexformFieldsRecursive($structure, $flexFormRowData)
+    protected function findFieldValue(string $givenFieldName, array $databaseRow, array $flexContext = [])
     {
-        $newStructure = [];
-        foreach ($structure as $key => $value) {
-            if ($key === 'el' && is_array($value)) {
-                $newSubStructure = [];
-                foreach ($value as $subKey => $subValue) {
-                    if (!isset($subValue['displayCond']) || $this->evaluateDisplayCondition($subValue['displayCond'], $flexFormRowData, true)) {
-                        $newSubStructure[$subKey] = $subValue;
+        $fieldValue = null;
+
+        // Early return for "normal" tca fields
+        if (empty($flexContext)) {
+            if (array_key_exists($givenFieldName, $databaseRow)) {
+                $fieldValue = $databaseRow[$givenFieldName];
+            }
+            return $fieldValue;
+        }
+        if ($flexContext['context'] === 'flexSheet') {
+            // A display condition on a flex form sheet. Relatively simple: fieldName is either
+            // "parentRec.fieldName" pointing to a databaseRow field name, or "sheetName.fieldName" pointing
+            // to a field value from a neighbor field.
+            if (strpos($givenFieldName, 'parentRec.') === 0) {
+                $fieldName = substr($givenFieldName, 10);
+                if (array_key_exists($fieldName, $databaseRow)) {
+                    $fieldValue = $databaseRow[$fieldName];
+                }
+            } else {
+                if (array_key_exists($givenFieldName, $flexContext['sheetNameFieldNames'])) {
+                    if ($flexContext['currentSheetName'] === $flexContext['sheetNameFieldNames'][$givenFieldName]['sheetName']) {
+                        throw new \RuntimeException(
+                            'Configuring displayCond to "' . $givenFieldName . '" on flex form sheet "'
+                            . $flexContext['currentSheetName'] . '" referencing a value from the same sheet does not make sense.',
+                            1481485705
+                        );
                     }
                 }
-                $value = $newSubStructure;
+                $sheetName = $flexContext['sheetNameFieldNames'][$givenFieldName]['sheetName'];
+                $fieldName = $flexContext['sheetNameFieldNames'][$givenFieldName]['fieldName'];
+                if (!isset($flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$fieldName]['vDEF'])) {
+                    throw new \RuntimeException(
+                        'Flex form displayCond on sheet "' . $flexContext['currentSheetName'] . '" references field "' . $fieldName
+                        . '" of sheet "' . $sheetName . '", but that field does not exist in current data structure',
+                        1481488492
+                    );
+                }
+                $fieldValue = $flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$fieldName]['vDEF'];
             }
-            if (is_array($value)) {
-                $value = $this->removeFlexformFieldsRecursive($value, $flexFormRowData);
+        } elseif ($flexContext['context'] === 'flexField') {
+            // A display condition on a flex field. Handle "parentRec." similar to sheet conditions,
+            // get a list of "local" field names and see if they are used as reference, else see if a
+            // "sheetName.fieldName" field reference is given
+            if (strpos($givenFieldName, 'parentRec.') === 0) {
+                $fieldName = substr($givenFieldName, 10);
+                if (array_key_exists($fieldName, $databaseRow)) {
+                    $fieldValue = $databaseRow[$fieldName];
+                }
+            } else {
+                $listOfLocalFlexFieldNames = array_keys(
+                    $flexContext['flexFormDataStructure']['sheets'][$flexContext['currentSheetName']]['ROOT']['el']
+                );
+                if (in_array($givenFieldName, $listOfLocalFlexFieldNames, true)) {
+                    // Condition references field name of the same sheet
+                    $sheetName = $flexContext['currentSheetName'];
+                    if (!isset($flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$givenFieldName]['vDEF'])) {
+                        throw new \RuntimeException(
+                            'Flex form displayCond on field "' . $flexContext['currentFieldName'] . '" on flex form sheet "'
+                            . $flexContext['currentSheetName'] . '" references field "' . $givenFieldName . '", but a field value'
+                            . ' does not exist in this sheet',
+                            1481492953
+                        );
+                    }
+                    $fieldValue = $flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$givenFieldName]['vDEF'];
+                } elseif (in_array($givenFieldName, array_keys($flexContext['sheetNameFieldNames'], true))) {
+                    // Condition references field name including a sheet name
+                    $sheetName = $flexContext['sheetNameFieldNames'][$givenFieldName]['sheetName'];
+                    $fieldName = $flexContext['sheetNameFieldNames'][$givenFieldName]['fieldName'];
+                    $fieldValue = $flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$fieldName]['vDEF'];
+                } else {
+                    throw new \RuntimeException(
+                        'Flex form displayCond on field "' . $flexContext['currentFieldName'] . '" on flex form sheet "'
+                        . $flexContext['currentSheetName'] . '" references a field or field / sheet combination "'
+                        . $givenFieldName . '" that might be defined in given data structure but is not found in data values.',
+                        1481496170
+                    );
+                }
+            }
+        } elseif ($flexContext['context'] === 'flexContainerElement') {
+            // A display condition on a flex form section container element. Handle "parentRec.", compare to a
+            // list of local field names, compare to a list of field names from same sheet, compare to a list
+            // of sheet fields from other sheets.
+            if (strpos($givenFieldName, 'parentRec.') === 0) {
+                $fieldName = substr($givenFieldName, 10);
+                if (array_key_exists($fieldName, $databaseRow)) {
+                    $fieldValue = $databaseRow[$fieldName];
+                }
+            } else {
+                $currentSheetName = $flexContext['currentSheetName'];
+                $currentFieldName = $flexContext['currentFieldName'];
+                $currentContainerIdentifier = $flexContext['currentContainerIdentifier'];
+                $currentContainerElementName = $flexContext['currentContainerElementName'];
+                $listOfLocalContainerElementNames = array_keys(
+                    $flexContext['flexFormDataStructure']['sheets'][$currentSheetName]['ROOT']
+                        ['el'][$currentFieldName]
+                        ['children'][$currentContainerIdentifier]
+                        ['el']
+                );
+                $listOfLocalContainerElementNamesWithSheetName = [];
+                foreach ($listOfLocalContainerElementNames as $aContainerElementName) {
+                    $listOfLocalContainerElementNamesWithSheetName[$currentSheetName . '.' . $aContainerElementName] = [
+                        'containerElementName' => $aContainerElementName,
+                    ];
+                }
+                $listOfLocalFlexFieldNames = array_keys(
+                    $flexContext['flexFormDataStructure']['sheets'][$currentSheetName]['ROOT']['el']
+                );
+                if (in_array($givenFieldName, $listOfLocalContainerElementNames, true)) {
+                    // Condition references field of same container instance
+                    $containerType = array_shift(array_keys(
+                        $flexContext['flexFormRowData']['data'][$currentSheetName]
+                            ['lDEF'][$currentFieldName]
+                            ['el'][$currentContainerIdentifier]
+                    ));
+                    $fieldValue = $flexContext['flexFormRowData']['data'][$currentSheetName]
+                        ['lDEF'][$currentFieldName]
+                        ['el'][$currentContainerIdentifier]
+                        [$containerType]
+                        ['el'][$givenFieldName]['vDEF'];
+                } elseif (in_array($givenFieldName, array_keys($listOfLocalContainerElementNamesWithSheetName, true))) {
+                    // Condition references field name of same container instance and has sheet name included
+                    $containerType = array_shift(array_keys(
+                        $flexContext['flexFormRowData']['data'][$currentSheetName]
+                        ['lDEF'][$currentFieldName]
+                        ['el'][$currentContainerIdentifier]
+                    ));
+                    $fieldName = $listOfLocalContainerElementNamesWithSheetName[$givenFieldName]['containerElementName'];
+                    $fieldValue = $flexContext['flexFormRowData']['data'][$currentSheetName]
+                        ['lDEF'][$currentFieldName]
+                        ['el'][$currentContainerIdentifier]
+                        [$containerType]
+                        ['el'][$fieldName]['vDEF'];
+                } elseif (in_array($givenFieldName, $listOfLocalFlexFieldNames, true)) {
+                    // Condition reference field name of sheet this section container is in
+                    $fieldValue = $flexContext['flexFormRowData']['data'][$currentSheetName]
+                        ['lDEF'][$givenFieldName]['vDEF'];
+                } elseif (in_array($givenFieldName, array_keys($flexContext['sheetNameFieldNames'], true))) {
+                    $sheetName = $flexContext['sheetNameFieldNames'][$givenFieldName]['sheetName'];
+                    $fieldName = $flexContext['sheetNameFieldNames'][$givenFieldName]['fieldName'];
+                    $fieldValue = $flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$fieldName]['vDEF'];
+                } else {
+                    $containerType = array_shift(array_keys(
+                        $flexContext['flexFormRowData']['data'][$currentSheetName]
+                        ['lDEF'][$currentFieldName]
+                        ['el'][$currentContainerIdentifier]
+                    ));
+                    throw new \RuntimeException(
+                        'Flex form displayCond on section container field "' . $currentContainerElementName . '" of container type "'
+                        . $containerType . '" on flex form sheet "'
+                        . $flexContext['currentSheetName'] . '" references a field or field / sheet combination "'
+                        . $givenFieldName . '" that might be defined in given data structure but is not found in data values.',
+                        1481634649
+                    );
+                }
             }
-            $newStructure[$key] = $value;
         }
 
-        return $newStructure;
+        return $fieldValue;
     }
 
     /**
-     * Flatten the Flexform data row for sheet level display conditions that use SheetName.FieldName
+     * Loop through TCA, find prepared conditions and evaluate them. Delete either the
+     * field itself if the condition did not match, or the 'displayCond' in TCA.
      *
-     * @param array $flexFormRowData
+     * @param array $result
      * @return array
      */
-    protected function flattenFlexformRowData($flexFormRowData)
+    protected function evaluateConditions(array $result): array
     {
-        $flatFlexFormRowData = [];
-        foreach ($flexFormRowData as $sheetName => $sheetConfiguration) {
-            foreach ($sheetConfiguration['lDEF'] as $fieldName => $fieldConfiguration) {
-                $flatFlexFormRowData[$sheetName . '.' . $fieldName] = $fieldConfiguration;
+        // Evaluate normal tca fields first
+        $listOfFlexFieldNames = [];
+        foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) {
+            $conditionResult = true;
+            if (isset($columnConfiguration['displayCond'])) {
+                $conditionResult = $this->evaluateConditionRecursive($columnConfiguration['displayCond']);
+                if (!$conditionResult) {
+                    unset($result['processedTca']['columns'][$columnName]);
+                } else {
+                    // Always unset the whole parsed display condition to save some memory, we're done with them
+                    unset($result['processedTca']['columns'][$columnName]['displayCond']);
+                }
+            }
+            // If field was not removed and if it is a flex field, add to list of flex fields to scan
+            if ($conditionResult && $columnConfiguration['config']['type'] === 'flex') {
+                $listOfFlexFieldNames[] = $columnName;
             }
         }
 
-        return $flatFlexFormRowData;
-    }
-
-    /**
-     * Evaluates the provided condition and returns TRUE if the form
-     * element should be displayed.
-     *
-     * The condition string is separated by colons and the first part
-     * indicates what type of evaluation should be performed.
-     *
-     * @param string $displayCondition
-     * @param array $record
-     * @param bool $flexformContext
-     * @param int $recursionLevel Internal level of recursion
-     * @return bool TRUE if condition evaluates successfully
-     */
-    protected function evaluateDisplayCondition($displayCondition, array $record = [], $flexformContext = false, $recursionLevel = 0)
-    {
-        if ($recursionLevel > 99) {
-            // This should not happen, treat as misconfiguration
-            return true;
+        // Search for flex fields and evaluate sheet conditions throwing them away if needed
+        foreach ($listOfFlexFieldNames as $columnName) {
+            $columnConfiguration = $result['processedTca']['columns'][$columnName];
+            foreach ($columnConfiguration['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) {
+                if (is_array($sheetConfiguration['ROOT']['displayCond'])) {
+                    if (!$this->evaluateConditionRecursive($sheetConfiguration['ROOT']['displayCond'])) {
+                        unset($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'][$sheetName]);
+                    } else {
+                        unset($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'][$sheetName]['ROOT']['displayCond']);
+                    }
+                }
+            }
         }
-        if (!is_array($displayCondition)) {
-            // DisplayCondition is not an array - just get its value
-            $result = $this->evaluateSingleDisplayCondition($displayCondition, $record, $flexformContext);
-        } else {
-            // Multiple conditions given as array ('AND|OR' => condition array)
-            $conditionEvaluations = [
-                'AND' => [],
-                'OR' => [],
-            ];
-            foreach ($displayCondition as $logicalOperator => $groupedDisplayConditions) {
-                $logicalOperator = strtoupper($logicalOperator);
-                if (($logicalOperator !== 'AND' && $logicalOperator !== 'OR') || !is_array($groupedDisplayConditions)) {
-                    // Invalid line. Skip it.
-                    continue;
-                } else {
-                    foreach ($groupedDisplayConditions as $key => $singleDisplayCondition) {
-                        $key = strtoupper($key);
-                        if (($key === 'AND' || $key === 'OR') && is_array($singleDisplayCondition)) {
-                            // Recursion statement: condition is 'AND' or 'OR' and is pointing to an array (should be conditions again)
-                            $conditionEvaluations[$logicalOperator][] = $this->evaluateDisplayCondition(
-                                [$key => $singleDisplayCondition],
-                                $record,
-                                $flexformContext,
-                                $recursionLevel + 1
-                            );
-                        } else {
-                            // Condition statement: collect evaluation of this single condition.
-                            $conditionEvaluations[$logicalOperator][] = $this->evaluateSingleDisplayCondition(
-                                $singleDisplayCondition,
-                                $record,
-                                $flexformContext
-                            );
+
+        // With full sheets gone we loop over display conditions of single fields in flex to throw fields away if needed
+        $listOfFlexSectionContainers = [];
+        foreach ($listOfFlexFieldNames as $columnName) {
+            $columnConfiguration = $result['processedTca']['columns'][$columnName];
+            if (is_array($columnConfiguration['config']['ds']['sheets'])) {
+                foreach ($columnConfiguration['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) {
+                    if (is_array($sheetConfiguration['ROOT']['el'])) {
+                        foreach ($sheetConfiguration['ROOT']['el'] as $flexField => $flexConfiguration) {
+                            $conditionResult = true;
+                            if (is_array($flexConfiguration['displayCond'])) {
+                                $conditionResult = $this->evaluateConditionRecursive($flexConfiguration['displayCond']);
+                                if (!$conditionResult) {
+                                    unset(
+                                        $result['processedTca']['columns'][$columnName]['config']['ds']
+                                            ['sheets'][$sheetName]['ROOT']
+                                            ['el'][$flexField]
+                                    );
+                                } else {
+                                    unset(
+                                        $result['processedTca']['columns'][$columnName]['config']['ds']
+                                            ['sheets'][$sheetName]['ROOT']
+                                            ['el'][$flexField]['displayCond']
+                                    );
+                                }
+                            }
+                            // If it was not removed and if the field is a section container, add it to the section container list
+                            if ($conditionResult
+                                && isset($flexConfiguration['type']) && $flexConfiguration['type'] === 'array'
+                                && isset($flexConfiguration['section']) && $flexConfiguration['section'] == 1
+                                && isset($flexConfiguration['children']) && is_array($flexConfiguration['children'])
+                            ) {
+                                $listOfFlexSectionContainers[] = [
+                                    'columnName' => $columnName,
+                                    'sheetName' => $sheetName,
+                                    'flexField' => $flexField,
+                                ];
+                            }
                         }
                     }
                 }
             }
-            if (!empty($conditionEvaluations['OR']) && in_array(true, $conditionEvaluations['OR'], true)) {
-                // There are OR conditions and at least one of them is TRUE
-                $result = true;
-            } elseif (!empty($conditionEvaluations['AND']) && !in_array(false, $conditionEvaluations['AND'], true)) {
-                // There are AND conditions and none of them is FALSE
-                $result = true;
-            } elseif (!empty($conditionEvaluations['OR']) || !empty($conditionEvaluations['AND'])) {
-                // There are some conditions. But no OR was TRUE and at least one AND was FALSE
-                $result = false;
-            } else {
-                // There are no proper conditions - misconfiguration. Return TRUE.
-                $result = true;
+        }
+
+        // Loop over found section container elements and evaluate their conditions
+        foreach ($listOfFlexSectionContainers as $flexSectionContainerPosition) {
+            $columnName = $flexSectionContainerPosition['columnName'];
+            $sheetName = $flexSectionContainerPosition['sheetName'];
+            $flexField = $flexSectionContainerPosition['flexField'];
+            $sectionElement = $result['processedTca']['columns'][$columnName]['config']['ds']
+                ['sheets'][$sheetName]['ROOT']
+                ['el'][$flexField];
+            foreach ($sectionElement['children'] as $containerInstanceName => $containerDataStructure) {
+                if (isset($containerDataStructure['el']) && is_array($containerDataStructure['el'])) {
+                    foreach ($containerDataStructure['el'] as $containerElementName => $containerElementConfiguration) {
+                        if (is_array($containerElementConfiguration['displayCond'])) {
+                            if (!$this->evaluateConditionRecursive($containerElementConfiguration['displayCond'])) {
+                                unset(
+                                    $result['processedTca']['columns'][$columnName]['config']['ds']
+                                        ['sheets'][$sheetName]['ROOT']
+                                        ['el'][$flexField]
+                                        ['children'][$containerInstanceName]
+                                        ['el'][$containerElementName]
+                                );
+                            } else {
+                                unset(
+                                    $result['processedTca']['columns'][$columnName]['config']['ds']
+                                        ['sheets'][$sheetName]['ROOT']
+                                        ['el'][$flexField]
+                                        ['children'][$containerInstanceName]
+                                        ['el'][$containerElementName]['displayCond']
+                                );
+                            }
+                        }
+                    }
+                }
             }
         }
+
         return $result;
     }
 
     /**
-     * Evaluates the provided condition and returns TRUE if the form
-     * element should be displayed.
+     * Evaluate a condition recursive by evaluating the single condition type
      *
-     * The condition string is separated by colons and the first part
-     * indicates what type of evaluation should be performed.
-     *
-     * @param string $displayCondition
-     * @param array $record
-     * @param bool $flexformContext
-     * @return bool
-     * @see evaluateDisplayCondition()
+     * @param array $conditionArray The condition to evaluate, possibly with subConditions for AND and OR types
+     * @return bool true if the condition matched
      */
-    protected function evaluateSingleDisplayCondition($displayCondition, array $record = [], $flexformContext = false)
+    protected function evaluateConditionRecursive(array $conditionArray): bool
     {
-        $result = false;
-        list($matchType, $condition) = explode(':', $displayCondition, 2);
-        switch ($matchType) {
+        switch ($conditionArray['type']) {
+            case 'AND':
+                $result = true;
+                foreach ($conditionArray['subConditions'] as $subCondition) {
+                    $result = $result && $this->evaluateConditionRecursive($subCondition);
+                }
+                return $result;
+            case 'OR':
+                $result = false;
+                foreach ($conditionArray['subConditions'] as $subCondition) {
+                    $result = $result || $this->evaluateConditionRecursive($subCondition);
+                }
+                return $result;
             case 'FIELD':
-                $result = $this->matchFieldCondition($condition, $record, $flexformContext);
-                break;
+                return $this->matchFieldCondition($conditionArray);
             case 'HIDE_FOR_NON_ADMINS':
-                $result = $this->matchHideForNonAdminsCondition();
-                break;
+                return (bool)$this->getBackendUser()->isAdmin();
             case 'REC':
-                $result = $this->matchRecordCondition($condition, $record);
-                break;
+                return $this->matchRecordCondition($conditionArray);
             case 'VERSION':
-                $result = $this->matchVersionCondition($condition, $record);
-                break;
+                return $this->matchVersionCondition($conditionArray);
             case 'USER':
-                $result = $this->matchUserCondition($condition, $record);
-                break;
+                return $this->matchUserCondition($conditionArray);
         }
-        return $result;
+        return false;
     }
 
     /**
      * Evaluates conditions concerning a field of the current record.
-     * Requires a record set via ->setRecord()
      *
      * Example:
      * "FIELD:sys_language_uid:>:0" => TRUE, if the field 'sys_language_uid' is greater than 0
      *
-     * @param string $condition
-     * @param array $record
-     * @param bool $flexformContext
+     * @param array $condition Condition array
      * @return bool
      */
-    protected function matchFieldCondition($condition, $record, $flexformContext = false)
+    protected function matchFieldCondition(array $condition): bool
     {
-        list($fieldName, $operator, $operand) = explode(':', $condition, 3);
-        if ($flexformContext) {
-            if (strpos($fieldName, 'parentRec.') !== false) {
-                $fieldNameParts = explode('.', $fieldName, 2);
-                $fieldValue = $record['parentRec'][$fieldNameParts[1]];
-            } else {
-                $fieldValue = $record[$fieldName]['vDEF'];
-            }
-        } else {
-            $fieldValue = $record[$fieldName];
-        }
+        $operator = $condition['operator'];
+        $operand = $condition['operand'];
+        $fieldValue = $condition['fieldValue'];
         $result = false;
         switch ($operator) {
             case 'REQ':
                 if (is_array($fieldValue) && count($fieldValue) <= 1) {
                     $fieldValue = array_shift($fieldValue);
                 }
-                if (strtoupper($operand) === 'TRUE') {
+                if ($operand) {
                     $result = (bool)$fieldValue;
                 } else {
                     $result = !$fieldValue;
@@ -337,7 +803,13 @@ class EvaluateDisplayConditions implements FormDataProviderInterface
                 if (is_array($fieldValue) && count($fieldValue) <= 1) {
                     $fieldValue = array_shift($fieldValue);
                 }
-                $result = $fieldValue >= $operand;
+                if ($fieldValue === null) {
+                    // If field value is null, this is NOT greater than or equal 0
+                    // See test set "Field is not greater than or equal to zero if empty array given"
+                    $result = false;
+                } else {
+                    $result = $fieldValue >= $operand;
+                }
                 break;
             case '<=':
                 if (is_array($fieldValue) && count($fieldValue) <= 1) {
@@ -350,8 +822,9 @@ class EvaluateDisplayConditions implements FormDataProviderInterface
                 if (is_array($fieldValue) && count($fieldValue) <= 1) {
                     $fieldValue = array_shift($fieldValue);
                 }
-                list($minimum, $maximum) = explode('-', $operand);
-                $result = $fieldValue >= $minimum && $fieldValue <= $maximum;
+                $min = $condition['min'];
+                $max = $condition['max'];
+                $result = $fieldValue >= $min && $fieldValue <= $max;
                 if ($operator[0] === '!') {
                     $result = !$result;
                 }
@@ -369,7 +842,7 @@ class EvaluateDisplayConditions implements FormDataProviderInterface
             case 'IN':
             case '!IN':
                 if (is_array($fieldValue)) {
-                    $result = count(array_intersect($fieldValue, explode(',', $operand))) > 0;
+                    $result = count(array_intersect($fieldValue, GeneralUtility::trimExplode(',', $operand))) > 0;
                 } else {
                     $result = GeneralUtility::inList($operand, $fieldValue);
                 }
@@ -389,71 +862,48 @@ class EvaluateDisplayConditions implements FormDataProviderInterface
     }
 
     /**
-     * Evaluates TRUE if current backend user is an admin.
-     *
-     * @return bool
-     */
-    protected function matchHideForNonAdminsCondition()
-    {
-        return (bool)$this->getBackendUser()->isAdmin();
-    }
-
-    /**
      * Evaluates conditions concerning the status of the current record.
-     * Requires a record set via ->setRecord()
      *
      * Example:
      * "REC:NEW:FALSE" => TRUE, if the record is already persisted (has a uid > 0)
      *
-     * @param string $condition
-     * @param array $record
+     * @param array $condition Condition array
      * @return bool
      */
-    protected function matchRecordCondition($condition, $record)
+    protected function matchRecordCondition(array $condition): bool
     {
-        $result = false;
-        list($operator, $operand) = explode(':', $condition, 2);
-        if ($operator === 'NEW') {
-            if (strtoupper($operand) === 'TRUE') {
-                $result = !((int)$record['uid'] > 0);
-            } elseif (strtoupper($operand) === 'FALSE') {
-                $result = ((int)$record['uid'] > 0);
-            }
+        if ($condition['isNew']) {
+            return !((int)$condition['uid'] > 0);
+        } else {
+            return (int)$condition['uid'] > 0;
         }
-        return $result;
     }
 
     /**
      * Evaluates whether the current record is versioned.
-     * Requires a record set via ->setRecord()
      *
-     * @param string $condition
-     * @param array $record
+     * @param array $condition Condition array
      * @return bool
      */
-    protected function matchVersionCondition($condition, $record)
+    protected function matchVersionCondition(array $condition): bool
     {
-        $result = false;
-        list($operator, $operand) = explode(':', $condition, 2);
-        if ($operator === 'IS') {
-            $isNewRecord = !((int)$record['uid'] > 0);
-            // Detection of version can be done be detecting the workspace of the user
-            $isUserInWorkspace = $this->getBackendUser()->workspace > 0;
-            if ((int)$record['pid'] === -1 || (int)$record['_ORIG_pid'] === -1) {
-                $isRecordDetectedAsVersion = true;
-            } else {
-                $isRecordDetectedAsVersion = false;
-            }
-            // New records in a workspace are not handled as a version record
-            // if it's no new version, we detect versions like this:
-            // -- if user is in workspace: always TRUE
-            // -- if editor is in live ws: only TRUE if pid == -1
-            $isVersion = ($isUserInWorkspace || $isRecordDetectedAsVersion) && !$isNewRecord;
-            if (strtoupper($operand) === 'TRUE') {
-                $result = $isVersion;
-            } elseif (strtoupper($operand) === 'FALSE') {
-                $result = !$isVersion;
-            }
+        $isNewRecord = !((int)$condition['uid'] > 0);
+        // Detection of version can be done by detecting the workspace of the user
+        $isUserInWorkspace = $this->getBackendUser()->workspace > 0;
+        if ((array_key_exists('pid', $condition) && (int)$condition['pid'] === -1)
+            || (array_key_exists('_ORIG_pid', $condition) && (int)$condition['_ORIG_pid'] === -1)
+        ) {
+            $isRecordDetectedAsVersion = true;
+        } else {
+            $isRecordDetectedAsVersion = false;
+        }
+        // New records in a workspace are not handled as a version record
+        // if it's no new version, we detect versions like this:
+        // * if user is in workspace: always TRUE
+        // * if editor is in live ws: only TRUE if pid == -1
+        $result = ($isUserInWorkspace || $isRecordDetectedAsVersion) && !$isNewRecord;
+        if (!$condition['isVersion']) {
+            $result = !$result;
         }
         return $result;
     }
@@ -461,22 +911,17 @@ class EvaluateDisplayConditions implements FormDataProviderInterface
     /**
      * Evaluates via the referenced user-defined method
      *
-     * @param string $condition
-     * @param array $record
+     * @param array $condition Condition array
      * @return bool
      */
-    protected function matchUserCondition($condition, $record)
+    protected function matchUserCondition(array $condition): bool
     {
-        $conditionParameters = explode(':', $condition);
-        $userFunction = array_shift($conditionParameters);
-
         $parameter = [
-            'record' => $record,
+            'record' => $condition['record'],
             'flexformValueKey' => 'vDEF',
-            'conditionParameters' => $conditionParameters
+            'conditionParameters' => $condition['parameters'],
         ];
-
-        return (bool)GeneralUtility::callUserFunction($userFunction, $parameter, $this);
+        return (bool)GeneralUtility::callUserFunction($condition['function'], $parameter, $this);
     }
 
     /**
index b408267..c0450ce 100644 (file)
@@ -29,12 +29,13 @@ class TcaFlexProcess implements FormDataProviderInterface
 {
     /**
      * Determine possible pageTsConfig overrides and apply them to ds.
-     * Determine available languages and sanitize dv for further processing. Then kick
+     * Determine available languages and sanitize ds for further processing. Then kick
      * and validate further details like excluded fields. Finally for each possible
-     * value and ds call FormDataCompiler with set FlexFormSegment group to resolve
+     * value and ds, call FormDataCompiler with set FlexFormSegment group to resolve
      * single field stuff like item processor functions.
      *
      * @param array $result
+     * @throws \RuntimeException
      * @return array
      */
     public function addData(array $result)
@@ -43,60 +44,161 @@ class TcaFlexProcess implements FormDataProviderInterface
             if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'flex') {
                 continue;
             }
-
-            $flexIdentifier = $this->getFlexIdentifier($result, $fieldName);
-            $pageTsConfigOfFlex = $this->getPageTsOfFlex($result, $fieldName, $flexIdentifier);
+            if (!isset($result['processedTca']['columns'][$fieldName]['config']['dataStructureIdentifier'])) {
+                throw new \RuntimeException(
+                    'Data structure identifier must be set, typically by executing TcaFlexPrepare data provider before',
+                    1480765571
+                );
+            }
+            $this->scanForInvalidSectionContainerTca($result, $fieldName);
+            $dataStructureIdentifier = $result['processedTca']['columns'][$fieldName]['config']['dataStructureIdentifier'];
+            $simpleDataStructureIdentifier = $this->getSimplifiedDataStructureIdentifier($dataStructureIdentifier);
+            $pageTsConfigOfFlex = $this->getPageTsOfFlex($result, $fieldName, $simpleDataStructureIdentifier);
             $result = $this->modifyOuterDataStructure($result, $fieldName, $pageTsConfigOfFlex);
-            $result = $this->removeExcludeFieldsFromDataStructure($result, $fieldName, $flexIdentifier);
+            $result = $this->removeExcludeFieldsFromDataStructure($result, $fieldName, $simpleDataStructureIdentifier);
             $result = $this->removeDisabledFieldsFromDataStructure($result, $fieldName, $pageTsConfigOfFlex);
+            // A "normal" call opening a record: Process data structure and field values
+            // This is called for "new" container ajax request too, since display conditions from section container
+            // elements can access record values of other flex form sheets and we need their values then.
             $result = $this->modifyDataStructureAndDataValuesByFlexFormSegmentGroup($result, $fieldName, $pageTsConfigOfFlex);
-            $result = $this->addDataStructurePointersToMetaData($result, $fieldName);
+            if (!empty($result['flexSectionContainerPreparation'])) {
+                // Create data and default values for a new section container, set by FormFlexAjaxController
+                $result = $this->prepareNewSectionContainer($result, $fieldName);
+            }
         }
 
         return $result;
     }
 
     /**
-     * Take care of ds_pointerField and friends to determine the correct sub array within
-     * TCA config ds.
-     *
-     * Gets extension identifier. Use second pointer field if it's value is not empty, "list" or "*",
-     * else it must be a plugin and first one will be used.
-     * This code basically determines the sub key of ds field:
-     * config = array(
-     *  ds => array(
-     *    'aFlexConfig' => '<flexXml ...
-     *     ^^^^^^^^^^^
-     * $flexformIdentifier contains "aFlexConfig" after this operation.
-     *
-     * @todo: This method is only implemented half. It basically should do all the
-     * @todo: pointer handling that is done within BackendUtility::getFlexFormDS() to $srcPointer.
-     * @todo: This can be solved now by adding 'identifier' from TcaFlexPrepare to 'config' array
+     * Some TCA combinations like inline or nesting a section into a section container is not
+     * supported and throws exceptions.
      *
      * @param array $result Result array
-     * @param string $fieldName Current handle field name
-     * @return string Pointer
+     * @param string $fieldName Handled field name
+     * @return void
+     * @throws \UnexpectedValueException
      */
-    protected function getFlexIdentifier(array $result, $fieldName)
+    protected function scanForInvalidSectionContainerTca(array $result, string $fieldName)
     {
-        // @todo: Current implementation with the "list_type, CType" fallback is rather limited and customized for
-        // @todo: tt_content, also it forces a ds_pointerField to be defined and a casual "default" sub array does not work
-        $pointerFields = !empty($result['processedTca']['columns'][$fieldName]['config']['ds_pointerField'])
-            ? $result['processedTca']['columns'][$fieldName]['config']['ds_pointerField']
-            : 'list_type,CType';
-        $pointerFields = GeneralUtility::trimExplode(',', $pointerFields);
-        $flexformIdentifier = !empty($result['databaseRow'][$pointerFields[0]]) ? $result['databaseRow'][$pointerFields[0]] : '';
-        if (!empty($result['databaseRow'][$pointerFields[1]])
-            && $result['databaseRow'][$pointerFields[1]] !== 'list'
-            && $result['databaseRow'][$pointerFields[1]] !== '*'
-        ) {
-            $flexformIdentifier = $result['databaseRow'][$pointerFields[1]];
+        $dataStructure = $result['processedTca']['columns'][$fieldName]['config']['ds'];
+        if (!isset($dataStructure['sheets']) || !is_array($dataStructure['sheets'])) {
+            return;
         }
-        if (empty($flexformIdentifier)) {
-            $flexformIdentifier = 'default';
+        foreach ($dataStructure['sheets'] as $dataStructureSheetName => $dataStructureSheetDefinition) {
+            if (!isset($dataStructureSheetDefinition['ROOT']['el']) || !is_array($dataStructureSheetDefinition['ROOT']['el'])) {
+                continue;
+            }
+            $dataStructureFields = $dataStructureSheetDefinition['ROOT']['el'];
+            foreach ($dataStructureFields as $dataStructureFieldName => $dataStructureFieldDefinition) {
+                if (isset($dataStructureFieldDefinition['type']) && $dataStructureFieldDefinition['type'] === 'array'
+                    && isset($dataStructureFieldDefinition['section']) && (string)$dataStructureFieldDefinition['section'] === '1'
+                ) {
+                    if (isset($dataStructureFieldDefinition['el']) && is_array($dataStructureFieldDefinition['el'])) {
+                        foreach ($dataStructureFieldDefinition['el'] as $containerName => $containerConfiguration) {
+                            if (isset($containerConfiguration['el']) && is_array($containerConfiguration['el'])) {
+                                foreach ($containerConfiguration['el'] as $singleFieldName => $singleFieldConfiguration) {
+                                    // Nesting type=inline in container sections is not supported. Throw an exception if configured.
+                                    if (isset($singleFieldConfiguration['config']['type']) && $singleFieldConfiguration['config']['type'] === 'inline') {
+                                        throw new \UnexpectedValueException(
+                                            'Invalid flex form data structure on field name "' . $fieldName . '" with element "' . $singleFieldName . '"'
+                                            . ' in section container "' . $containerName . '": Nesting inline elements in flex form'
+                                            . ' sections is not allowed.',
+                                            1458745468
+                                        );
+                                    }
+
+                                    // Nesting sections is not supported. Throw an exception if configured.
+                                    if (is_array($singleFieldConfiguration)
+                                        && isset($singleFieldConfiguration['type']) && $singleFieldConfiguration['type'] === 'array'
+                                        && isset($singleFieldConfiguration['section']) && (string)$singleFieldConfiguration['section'] === '1'
+                                    ) {
+                                        throw new \UnexpectedValueException(
+                                            'Invalid flex form data structure on field name "' . $fieldName . '" with element "' . $singleFieldName . '"'
+                                            . ' in section container "' . $containerName . '": Nesting sections in container elements'
+                                            . ' sections is not allowed.',
+                                            1458745712
+                                        );
+                                    }
+
+                                    // Nesting type="select" and type="group" within section containers is not supported,
+                                    // the data storage can not deal with that and in general it is not supported to add a
+                                    // named reference to the anonymous section container structure.
+                                    if (is_array($singleFieldConfiguration)
+                                        && isset($singleFieldConfiguration['config']['type'])
+                                        && ($singleFieldConfiguration['config']['type'] === 'group' || $singleFieldConfiguration['config']['type'] === 'select')
+                                        && array_key_exists('MM', $singleFieldConfiguration['config'])
+                                    ) {
+                                        throw new \UnexpectedValueException(
+                                            'Invalid flex form data structure on field name "' . $fieldName . '" with element "' . $singleFieldName . '"'
+                                            . ' in section container "' . $containerName . '": Nesting select and group elements in flex form'
+                                            . ' sections is not allowed with MM relations.',
+                                            1481647089
+                                        );
+                                    }
+                                }
+                            }
+                        }
+                    }
+                } elseif (isset($dataStructureFieldDefinition['type']) || isset($dataStructureFieldDefinition['section'])) {
+                    // type without section is not ok
+                    throw new \UnexpectedValueException(
+                        'Broken data structure on field name ' . $fieldName . '. section without type or vice versa is not allowed',
+                        1440685208
+                    );
+                }
+            }
         }
+    }
 
-        return $flexformIdentifier;
+    /**
+     * Calculate a simplified (and wrong) data structure identifier.
+     * This is used to find pageTsConfig options of flex fields and exclude field definitions later, see methods below.
+     * If the data structure identifier is not type=tca based and if dataStructureKey is not as expected, fallback is "default"
+     *
+     * Example pi_flexform with ext:news in tt_content:
+     * * TCA config of pi_flexform ds_pointerfield is set to "list_type,CType"
+     * * list_type in databaseRow is "news_pi1"
+     * * CType in databaseRow is "list"
+     * * The resulting dataStructureIdentifier calculated by FlexFormTools is then:
+     *   {"type":"tca","tableName":"tt_content","fieldName":"pi_flexform","dataStructureKey":"news_pi1,list"}
+     * * The resulting simpleDataStructureIdentifier is "news_pi1"
+     * * The pageTsConfig base path used for flex field overrides is "TCEFORM.tt_content.pi_flexform.news_pi1", a full
+     *   example path disabling a field: "TCEFORM.tt_content.pi_flexform.news_pi1.sDEF.settings\.orderBy.disabled = 1"
+     * * The exclude path for be_user exclude rights is "tt_content:pi_flexform;news_pi1", a full example:
+     *   tt_content:pi_flexform;news_pi1;sDEF;settings.orderBy
+     *
+     * Notes:
+     * This approach is obviously limited. It is not possible to override flex form DS via pageTsConfig for other complex
+     * or dynamically created data structure definitions. And worse, the fallback to "default" may lead to naming clashes
+     * if two different data structures have identical sheet and field names.
+     * Also, the exclude field handling is limited and it is not possible to respect 'exclude' fields in flex form
+     * data structures if the dataStructureIdentifier is based on type="record" or manipulated by a hook in FlexFormTools.
+     * All that can only be solved by changing the pageTsConfig syntax referencing flex fields, probably by involving the whole
+     * data structure identifier and going away from this "simple" approach. For exclude fields there is the additional
+     * issue that the special="exclude" code is based on guess work, to find possible data structures. If this area here is
+     * changed and a pageTsConfig syntax change is raised, it would probably be a good idea to solve the access restrictions
+     * area at the same time - see the related methods that deal with flex field handling for special="exclude" for
+     * more comments on this.
+     * Another limitation is that the current syntax in both pageTsConfig and exclude fields does not
+     * consider flex form section containers at all.
+     *
+     * @param string $dataStructureIdentifier
+     * @return string
+     */
+    protected function getSimplifiedDataStructureIdentifier(string $dataStructureIdentifier): string
+    {
+        $identifierArray = json_decode($dataStructureIdentifier, true);
+        $simpleDataStructureIdentifier = 'default';
+        if (isset($identifierArray['type']) && $identifierArray['type'] === 'tca' && isset($identifierArray['dataStructureKey'])) {
+            $explodedKey = explode(',', $identifierArray['dataStructureKey']);
+            if (!empty($explodedKey[1]) && $explodedKey[1] !== 'list' && $explodedKey[1] !== '*') {
+                $simpleDataStructureIdentifier = $explodedKey[1];
+            } elseif (!empty($explodedKey[0]) && $explodedKey[0] !== 'list' && $explodedKey[0] !== '*') {
+                $simpleDataStructureIdentifier = $explodedKey[0];
+            }
+        }
+        return $simpleDataStructureIdentifier;
     }
 
     /**
@@ -227,10 +329,9 @@ class TcaFlexProcess implements FormDataProviderInterface
      * Feed single flex field and data to FlexFormSegment FormData compiler and merge result.
      * This one is nasty. Goal is to have processed TCA stuff in DS and also have validated / processed data values.
      *
-     * Three main parts in this method:
-     * * Process values of existing section container for default values
-     * * Process values and TCA of possible section container and create a default value row for each
-     * * Process TCA of "normal" fields and have default values in data ['templateRows']['containerName'] parallel to section ['el']
+     * Two main parts in this method:
+     * * Process values and TCA of existing section containers
+     * * Process TCA of "normal" fields
      *
      * @param array $result Result array
      * @param string $fieldName Current handle field name
@@ -248,16 +349,14 @@ class TcaFlexProcess implements FormDataProviderInterface
             return $result;
         }
 
-        /** @var FlexFormSegment $formDataGroup */
         $formDataGroup = GeneralUtility::makeInstance(FlexFormSegment::class);
-        /** @var FormDataCompiler $formDataCompiler */
         $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
 
         foreach ($dataStructure['sheets'] as $dataStructureSheetName => $dataStructureSheetDefinition) {
             if (!isset($dataStructureSheetDefinition['ROOT']['el']) || !is_array($dataStructureSheetDefinition['ROOT']['el'])) {
                 continue;
             }
-            $dataStructureSheetElements = $dataStructureSheetDefinition['ROOT']['el'];
+            $dataStructureFields = $dataStructureSheetDefinition['ROOT']['el'];
 
             // Prepare pageTsConfig of this sheet
             $pageTsConfig['TCEFORM.'][$tableName . '.'] = [];
@@ -310,35 +409,41 @@ class TcaFlexProcess implements FormDataProviderInterface
             $tcaValueArray = [
                 'uid' => $result['databaseRow']['uid'],
             ];
-            foreach ($dataStructureSheetElements as $dataStructureSheetElementName => $dataStructureSheetElementDefinition) {
-                if (isset($dataStructureSheetElementDefinition['type']) && $dataStructureSheetElementDefinition['type'] === 'array'
-                    && isset($dataStructureSheetElementDefinition['section']) && (string)$dataStructureSheetElementDefinition['section'] === '1'
+            foreach ($dataStructureFields as $dataStructureFieldName => $dataStructureFieldDefinition) {
+                if (isset($dataStructureFieldDefinition['type']) && $dataStructureFieldDefinition['type'] === 'array'
+                    && isset($dataStructureFieldDefinition['section']) && (string)$dataStructureFieldDefinition['section'] === '1'
                 ) {
-                    // A section
-
-                    // Existing section container elements
-                    if (isset($dataValues['data'][$dataStructureSheetName]['lDEF'][$dataStructureSheetElementName]['el'])
-                        && is_array($dataValues['data'][$dataStructureSheetName]['lDEF'][$dataStructureSheetElementName]['el'])
+                    // Existing section containers. Prepare data values and create a unique data structure per container.
+                    // This is important for instance for display conditions later enabling them to change ds per container instance.
+                    // In the end, the data values in
+                    // ['databaseRow']['aFieldName']['data']['aSheet']['lDEF']['aSectionField']['el']['aContainer']
+                    // are prepared, and additionally, the processedTca data structure is changed and has a specific container
+                    // name per container instance in
+                    // ['processedTca']['columns']['aFieldName']['config']['ds']['sheets']['aSheet']['ROOT']['el']['aSectionField']['children']['aContainer']
+                    if (isset($dataValues['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName]['el'])
+                        && is_array($dataValues['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName]['el'])
                     ) {
-                        $containerArray = $dataValues['data'][$dataStructureSheetName]['lDEF'][$dataStructureSheetElementName]['el'];
-                        foreach ($containerArray as $aContainerNumber => $aContainerArray) {
+                        $containerValueArray = $dataValues['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName]['el'];
+                        $containerDataStructuresPerContainer = [];
+                        foreach ($containerValueArray as $aContainerIdentifier => $aContainerArray) {
                             if (is_array($aContainerArray)) {
                                 foreach ($aContainerArray as $aContainerName => $aContainerElementArray) {
                                     if ($aContainerName === '_TOGGLE') {
                                         // Don't handle internal toggle state field
                                         continue;
                                     }
-                                    if (!isset($dataStructureSheetElements[$dataStructureSheetElementName]['el'][$aContainerName])) {
+                                    if (!isset($dataStructureFields[$dataStructureFieldName]['el'][$aContainerName])) {
                                         // Container not defined in ds
                                         continue;
                                     }
+                                    $vanillaContainerDataStructure = $dataStructureFields[$dataStructureFieldName]['el'][$aContainerName];
 
                                     $newColumns = [];
                                     $editColumns = [];
                                     $valueArray = [
                                         'uid' => $result['databaseRow']['uid'],
                                     ];
-                                    foreach ($dataStructureSheetElements[$dataStructureSheetElementName]['el'][$aContainerName]['el'] as $singleFieldName => $singleFieldConfiguration) {
+                                    foreach ($vanillaContainerDataStructure['el'] as $singleFieldName => $singleFieldConfiguration) {
                                         // $singleFieldValueArray = ['data']['sSections']['lDEF']['section_1']['el']['1']['container_1']['el']['element_1']
                                         $singleFieldValueArray = [];
                                         if (isset($aContainerElementArray['el'][$singleFieldName])
@@ -348,11 +453,11 @@ class TcaFlexProcess implements FormDataProviderInterface
                                         }
 
                                         if (array_key_exists('vDEF', $singleFieldValueArray)) {
-                                            $editColumns[$singleFieldName] = $singleFieldConfiguration;
                                             $valueArray[$singleFieldName] = $singleFieldValueArray['vDEF'];
                                         } else {
                                             $newColumns[$singleFieldName] = $singleFieldConfiguration;
                                         }
+                                        $editColumns[$singleFieldName] = $singleFieldConfiguration;
                                     }
 
                                     $inputToFlexFormSegment = [
@@ -365,28 +470,23 @@ class TcaFlexProcess implements FormDataProviderInterface
                                             'ctrl' => [],
                                             'columns' => [],
                                         ],
+                                        'selectTreeCompileItems' => $result['selectTreeCompileItems'],
                                         'flexParentDatabaseRow' => $result['databaseRow'],
                                     ];
 
                                     if (!empty($newColumns)) {
+                                        // This is scenario "field has been added to data structure, but field value does not exist in value array yet"
+                                        // We want that stuff like TCA "default" values are then applied to those fields. What we do here is
+                                        // calling the data compiler with those "new" fields to fetch their values and set them in value array.
+                                        // Those fields are then compiled a second time in the "edit" phase to prepare their final TCA.
+                                        // This two-phase compiling is needed to ensure that for instance display conditions work with
+                                        // fields that may just have been added to the data structure but are not yet initialized as data value.
                                         $inputToFlexFormSegment['command'] = 'new';
                                         $inputToFlexFormSegment['processedTca']['columns'] = $newColumns;
                                         $flexSegmentResult = $formDataCompiler->compile($inputToFlexFormSegment);
-
                                         foreach ($newColumns as $singleFieldName => $_) {
-                                            // Set data value result
-                                            if (array_key_exists($singleFieldName, $flexSegmentResult['databaseRow'])) {
-                                                $result['databaseRow'][$fieldName]
-                                                ['data'][$dataStructureSheetName]['lDEF'][$dataStructureSheetElementName]
-                                                ['el'][$aContainerNumber][$aContainerName]['el'][$singleFieldName]['vDEF'] =
-                                                    $flexSegmentResult['databaseRow'][$singleFieldName];
-                                            }
-                                            // Set TCA structure result, actually, this call *might* be obsolete since the "dummy"
-                                            // handling below will set it again.
-                                            $result['processedTca']['columns'][$fieldName]['config']['ds']
-                                            ['sheets'][$dataStructureSheetName]['ROOT']['el']
-                                            [$dataStructureSheetElementName]['el'][$aContainerName]['el'][$singleFieldName] =
-                                                $flexSegmentResult['processedTca']['columns'][$singleFieldName];
+                                            // Set data value result to feed it to "edit" next
+                                            $valueArray[$singleFieldName] = $flexSegmentResult['databaseRow'][$singleFieldName];
                                         }
                                     }
 
@@ -394,122 +494,47 @@ class TcaFlexProcess implements FormDataProviderInterface
                                         $inputToFlexFormSegment['command'] = 'edit';
                                         $inputToFlexFormSegment['processedTca']['columns'] = $editColumns;
                                         $flexSegmentResult = $formDataCompiler->compile($inputToFlexFormSegment);
-
                                         foreach ($editColumns as $singleFieldName => $_) {
-                                            // Set data value result
-                                            if (array_key_exists($singleFieldName, $flexSegmentResult['databaseRow'])) {
-                                                $result['databaseRow'][$fieldName]
-                                                ['data'][$dataStructureSheetName]['lDEF'][$dataStructureSheetElementName]
-                                                ['el'][$aContainerNumber][$aContainerName]['el'][$singleFieldName]['vDEF'] =
-                                                    $flexSegmentResult['databaseRow'][$singleFieldName];
-                                            }
-                                            // Set TCA structure result, actually, this call *might* be obsolete since the "dummy"
-                                            // handling below will set it again.
-                                            $result['processedTca']['columns'][$fieldName]['config']['ds']
-                                            ['sheets'][$dataStructureSheetName]['ROOT']['el']
-                                            [$dataStructureSheetElementName]['el'][$aContainerName]['el'][$singleFieldName] =
-                                                $flexSegmentResult['processedTca']['columns'][$singleFieldName];
+                                            $result['databaseRow'][$fieldName]
+                                                ['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName]
+                                                ['el'][$aContainerIdentifier][$aContainerName]['el'][$singleFieldName]['vDEF']
+                                                = $flexSegmentResult['databaseRow'][$singleFieldName];
+                                            $containerDataStructuresPerContainer[$aContainerIdentifier] = $vanillaContainerDataStructure;
+                                            $containerDataStructuresPerContainer[$aContainerIdentifier]['el'] = $flexSegmentResult['processedTca']['columns'];
                                         }
                                     }
                                 }
                             }
-                        }
-                        // End of existing data value handling
+                        } // End of existing data value handling
+                        // Set 'data structures per container' next to 'el' that contains vanilla data structures
+                        $result['processedTca']['columns'][$fieldName]['config']['ds']
+                            ['sheets'][$dataStructureSheetName]['ROOT']['el']
+                            [$dataStructureFieldName]['children'] = $containerDataStructuresPerContainer;
                     } else {
-                        // Force the section to be an empty array if there are no existing containers
+                        // Force the section data array to be an empty array if there are no existing containers
                         $result['databaseRow'][$fieldName]
-                        ['data'][$dataStructureSheetName]['lDEF'][$dataStructureSheetElementName]['el'] = [];
+                            ['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName]['el'] = [];
+                        // Force data structure array to be empty if there are no existing containers
+                        $result['processedTca']['columns'][$fieldName]['config']['ds']
+                            ['sheets'][$dataStructureSheetName]['ROOT']['el']
+                            [$dataStructureFieldName]['children'] = [];
                     }
 
-                    // Prepare "fresh" row for every possible container
-                    if (isset($dataStructureSheetElements[$dataStructureSheetElementName]['el']) && is_array($dataStructureSheetElements[$dataStructureSheetElementName]['el'])) {
-                        foreach ($dataStructureSheetElements[$dataStructureSheetElementName]['el'] as $possibleContainerName => $possibleContainerConfiguration) {
-                            if (isset($possibleContainerConfiguration['el']) && is_array($possibleContainerConfiguration['el'])) {
-                                // Initialize result data array templateRows
-                                $result['databaseRow'][$fieldName]
-                                ['data'][$dataStructureSheetName]['lDEF'][$dataStructureSheetElementName]['templateRows']
-                                [$possibleContainerName]['el']
-                                    = [];
-                                foreach ($possibleContainerConfiguration['el'] as $singleFieldName => $singleFieldConfiguration) {
-
-                                    // Nesting type=inline in container sections is not supported. Throw an exception if configured.
-                                    if (isset($singleFieldConfiguration['config']['type']) && $singleFieldConfiguration['config']['type'] === 'inline') {
-                                        throw new \UnexpectedValueException(
-                                            'Invalid flex form data structure on field name "' . $fieldName . '" with element "' . $singleFieldName . '"'
-                                                . ' in section container "' . $possibleContainerName . '": Nesting inline elements in flex form'
-                                                . ' sections is not allowed.',
-                                            1458745468
-                                        );
-                                    }
-
-                                    // Nesting sections is not supported. Throw an exception if configured.
-                                    if (is_array($singleFieldConfiguration)
-                                        && isset($singleFieldConfiguration['type']) && $singleFieldConfiguration['type'] === 'array'
-                                        && isset($singleFieldConfiguration['section']) && (string)$singleFieldConfiguration['section'] === '1'
-                                    ) {
-                                        throw new \UnexpectedValueException(
-                                            'Invalid flex form data structure on field name "' . $fieldName . '" with element "' . $singleFieldName . '"'
-                                                . ' in section container "' . $possibleContainerName . '": Nesting sections in container elements'
-                                                . ' sections is not allowed.',
-                                            1458745712
-                                        );
-                                    }
-
-                                    $inputToFlexFormSegment = [
-                                        'tableName' => $result['tableName'],
-                                        'command' => 'new',
-                                        'pageTsConfig' => [],
-                                        'databaseRow' => [
-                                            'uid' => $result['databaseRow']['uid'],
-                                        ],
-                                        'processedTca' => [
-                                            'ctrl' => [],
-                                            'columns' => [
-                                                $singleFieldName => $singleFieldConfiguration,
-                                            ],
-                                        ],
-                                        'selectTreeCompileItems' => false,
-                                        'flexParentDatabaseRow' => $result['databaseRow'],
-                                    ];
-                                    $flexSegmentResult = $formDataCompiler->compile($inputToFlexFormSegment);
-                                    if (array_key_exists($singleFieldName, $flexSegmentResult['databaseRow'])) {
-                                        $result['databaseRow'][$fieldName]
-                                        ['data'][$dataStructureSheetName]['lDEF'][$dataStructureSheetElementName]['templateRows']
-                                        [$possibleContainerName]['el'][$singleFieldName]['vDEF']
-                                         = $flexSegmentResult['databaseRow'][$singleFieldName];
-                                    }
-                                    $result['processedTca']['columns'][$fieldName]['config']['ds']
-                                    ['sheets'][$dataStructureSheetName]['ROOT']['el'][$dataStructureSheetElementName]['el']
-                                    [$possibleContainerName]['el'][$singleFieldName]
-                                        = $flexSegmentResult['processedTca']['columns'][$singleFieldName];
-                      &n