[!!!][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\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;
 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 = [];
             $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,
 
         $formDataCompilerInputForParent = [
             'vanillaUid' => $vanillaUid,
             'command' => $command,
             'tableName' => $parent['table'],
             'databaseRow' => $databaseRow,
+            'processedTca' => $processedTca,
             'inlineFirstPid' => $inlineFirstPid,
             'inlineFirstPid' => $inlineFirstPid,
-            'columnsToProcess' => array_merge(
-                [$parentFieldName],
-                array_keys($databaseRow)
-            ),
+            'columnsToProcess' => [
+                $parentFieldName,
+            ],
             // Do not resolve existing children, we don't need them now
             'inlineResolveExistingChildren' => false,
         ];
             // Do not resolve existing children, we don't need them now
             'inlineResolveExistingChildren' => false,
         ];
@@ -248,18 +258,26 @@ class FormInlineAjaxController
             'uid' => (int)$parent['uid'],
         ];
 
             '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,
 
         $formDataCompilerInputForParent = [
             'vanillaUid' => (int)$parent['uid'],
             'command' => 'edit',
             'tableName' => $parent['table'],
             'databaseRow' => $databaseRow,
+            'processedTca' => $processedTca,
             'inlineFirstPid' => $inlineFirstPid,
             '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
             // @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;
         }
         foreach ($childResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) {
             $jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost;
         }
-        $jsonResult['scriptCall'][] = $childResult['extJSCODE'];
         if (!empty($childResult['additionalInlineLanguageLabelFiles'])) {
             $labels = [];
             foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) {
         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 $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'])) {
     {
         if (!isset($ajaxArguments['context'])) {
-            return $databaseRow;
+            return '';
         }
 
         $context = json_decode($ajaxArguments['context'], true);
         if (GeneralUtility::hmac(serialize($context['config'])) !== $context['hmac']) {
         }
 
         $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\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
 
 /**
  * Receives ajax request from FormEngine suggest wizard and creates suggest answer as json result
@@ -41,75 +40,63 @@ class SuggestWizardController
     {
         $parsedBody = $request->getParsedBody();
 
     {
         $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'];
         $search = $parsedBody['value'];
-        $table = $parsedBody['table'];
-        $field = $parsedBody['field'];
+        $tableName = $parsedBody['tableName'];
+        $fieldName = $parsedBody['fieldName'];
         $uid = $parsedBody['uid'];
         $pid = (int)$parsedBody['pid'];
         $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'])) {
         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
 
         // Determine TCA config of field
-        if (empty($dataStructureIdentifierString)) {
+        if (empty($dataStructureIdentifier)) {
             // Normal columns field
             // 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);
         } 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);
         $wizardConfig = $fieldConfig['wizards']['suggest'];
 
         $queryTables = $this->getTablesToQueryFromFieldConfiguration($fieldConfig);
@@ -125,7 +112,7 @@ class SuggestWizardController
                 continue;
             }
 
                 continue;
             }
 
-            $config = $this->getConfigurationForTable($queryTable, $wizardConfig, $TSconfig, $table, $field);
+            $config = $this->getConfigurationForTable($queryTable, $wizardConfig, $pageTsConfig, $tableName, $fieldNameInPageTsConfig);
 
             // process addWhere
             if (!isset($config['addWhere']) && $whereClause) {
 
             // process addWhere
             if (!isset($config['addWhere']) && $whereClause) {
@@ -136,8 +123,8 @@ class SuggestWizardController
                     '###THIS_UID###' => (int)$uid,
                     '###CURRENT_PID###' => (int)$pid
                 ];
                     '###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'];
                     }
                     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.
      *
      * 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' => [],
             'stylesheetFiles' => [],
             // can hold strings or arrays, string = requireJS module, array = requireJS module + callback e.g. array('TYPO3/Foo/Bar', 'function() {}')
             'requireJsModules' => [],
-            'extJSCODE' => '',
             'inlineData' => [],
             'html' => '',
         ];
             '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;
         }
         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';
             $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'];
         }
         if (!empty($config['required'])) {
             $validationRules[] = ['type' => 'required'];
index e8a1d77..7b2f04b 100644 (file)
@@ -34,18 +34,16 @@ class FlexFormContainerContainer extends AbstractContainer
      */
     public function render()
     {
      */
     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'];
         $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'];
 
         $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()
         $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>';
 
             . $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]'
         $actionFieldName = '_ACTION_FLEX_FORM'
             . $parameterArray['itemFormElName']
             . $this->data['flexFormFormPrefix']
             . '[_ACTION]'
-            . '[' . $flexFormContainerCounter . ']';
+            . '[' . $flexFormContainerIdentifier . ']';
         $toggleFieldName = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']'
             . $flexFormFormPrefix
         $toggleFieldName = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']'
             . $flexFormFormPrefix
-            . '[' . $flexFormContainerCounter . ']'
+            . '[' . $flexFormContainerIdentifier . ']'
             . '[_TOGGLE]';
 
         $moveAndDeleteContent = [];
         $userHasAccessToDefaultLanguage = $this->getBackendUserAuthentication()->checkLanguageAccess(0);
         if ($userHasAccessToDefaultLanguage) {
             . '[_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 = $this->data;
-        $options['flexFormFieldIdentifierPrefix'] = $flexFormFieldIdentifierPrefix;
         // Append container specific stuff to field prefix
         // 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();
 
         $options['renderType'] = 'flexFormElementContainer';
         $containerContentResult = $this->nodeFactory->create($options)->render();
 
+        $containerTitle = '';
+        if (!empty(trim($flexFormDataStructureArray['title']))) {
+            $containerTitle = $languageService->sL(trim($flexFormDataStructureArray['title']));
+        }
+
         $html = [];
         $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">';
         $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[] =                    $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">';
         $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[] =        '</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') . '"';
         $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\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;
 
 /**
 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
  *
  * 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
  * section is handled and hands over to FlexFormSectionContainer in this case.
  */
 class FlexFormElementContainer extends AbstractContainer
@@ -62,16 +61,10 @@ class FlexFormElementContainer extends AbstractContainer
                     continue;
                 }
 
                     continue;
                 }
 
-                $sectionTitle = '';
-                if (!empty(trim($flexFormFieldArray['title']))) {
-                    $sectionTitle = $languageService->sL(trim($flexFormFieldArray['title']));
-                }
-
                 $options = $this->data;
                 $options = $this->data;
-                $options['flexFormDataStructureArray'] = $flexFormFieldArray['el'];
+                $options['flexFormDataStructureArray'] = $flexFormFieldArray;
                 $options['flexFormRowData'] = isset($flexFormRowData[$flexFormFieldName]['el']) ? $flexFormRowData[$flexFormFieldName]['el'] : [];
                 $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);
                 $options['renderType'] = 'flexFormSectionContainer';
                 $sectionContainerResult = $this->nodeFactory->create($options)->render();
                 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $sectionContainerResult);
@@ -90,13 +83,16 @@ class FlexFormElementContainer extends AbstractContainer
                 ];
 
                 $alertMsgOnChange = '';
                 ];
 
                 $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)) {
                     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();}';
                     }
                     } 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']);
                     }
                 }
                         $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'];
                 $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;
                 }
 
                 $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]';
 
                 $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()
     {
      */
     public function render()
     {
+        $flexFormDataStructureIdentifier = $this->data['parameterArray']['fieldConf']['config']['dataStructureIdentifier'];
         $flexFormDataStructureArray = $this->data['parameterArray']['fieldConf']['config']['ds'];
 
         $options = $this->data;
         $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';
         $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();
 
         $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'] ?: [];
         // 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 = $this->data;
         $options['flexFormDataStructureArray'] = $flexFormDataStructureArray['sheets'][$sheetName]['ROOT']['el'];
         $options['flexFormRowData'] = $flexFormRowDataSubPart;
+        $options['flexFormSheetName'] = $sheetName;
         $options['flexFormFormPrefix'] = '[data][' . $sheetName . '][lDEF]';
         $options['parameterArray'] = $parameterArray;
 
         $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\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
 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
 {
  */
 class FlexFormSectionContainer extends AbstractContainer
 {
@@ -38,133 +38,80 @@ class FlexFormSectionContainer extends AbstractContainer
      */
     public function render()
     {
      */
     public function render()
     {
-        /** @var IconFactory $iconFactory */
         $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
         $languageService = $this->getLanguageService();
 
         $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
         $languageService = $this->getLanguageService();
 
-        $flexFormFieldsArray = $this->data['flexFormDataStructureArray'];
+        $flexFormDataStructureIdentifier = $this->data['flexFormDataStructureIdentifier'];
+        $flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
         $flexFormRowData = $this->data['flexFormRowData'];
         $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();
 
 
         $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
         // 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
             // @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 = [];
         $containerTemplatesHtml = [];
-        foreach ($flexFormFieldsArray as $flexFormContainerName => $flexFormFieldDefinition) {
-            $containerTemplateHtml = [];
-            $sectionTitle = '';
+        foreach ($flexFormDataStructureArray['el'] as $flexFormContainerName => $flexFormFieldDefinition) {
+            $containerTitle = '';
             if (!empty(trim($flexFormFieldDefinition['title']))) {
             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[] =    $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);
             $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">';
         // 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>';
         }
 
             $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 = [];
         // 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[] =         '<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">';
         $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[] =                 '</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[] =             '>';
         $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 = $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
             $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'])
 
         // 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>]
         }
 
         // e.g. data[<table>][<uid>][<field>]
@@ -134,10 +134,6 @@ class InlineControlContainer extends AbstractContainer
         $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
 
         $config['inline']['first'] = false;
         $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'];
         $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']];
         // 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 {
 
             if (!empty($fileUid)) {
                 try {
@@ -419,14 +418,15 @@ class InlineRecordContainer extends AbstractContainer
                 . '</span>';
         }
         // "Info": (All records)
                 . '</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 ($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() . '
             $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',
                     ->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',
                         ),
                         $queryBuilder->expr()->eq(
                             'sys_language_uid',
index 21d3ac6..6dbfa06 100644 (file)
@@ -40,6 +40,7 @@ class SingleFieldContainer extends AbstractContainer
     /**
      * Entry method
      *
     /**
      * Entry method
      *
+     * @throws \InvalidArgumentException
      * @return array As defined in initializeResultArray() of AbstractNode
      */
     public function render()
      * @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)) {
             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;
                 $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 {
             } 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.
             $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)) {
         ) {
             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() };';
             }
             } 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
 /**
  * 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
 {
  */
 interface DatabaseFileIconsHookInterface
 {
index 5fe64b0..883ed97 100644 (file)
@@ -14,21 +14,16 @@ namespace TYPO3\CMS\Backend\Form\Element;
  * The TYPO3 project - inspiring people to share!
  */
 
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Backend\Clipboard\Clipboard;
 use TYPO3\CMS\Backend\Form\AbstractNode;
 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\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\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\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;
 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;
 
     /**
     protected $maxInputWidth = 50;
 
     /**
-     * @var \TYPO3\CMS\Backend\Clipboard\Clipboard|NULL
-     */
-    protected $clipboard = null;
-
-    /**
      * @var NodeFactory
      */
     protected $nodeFactory;
      * @var NodeFactory
      */
     protected $nodeFactory;
@@ -387,15 +377,8 @@ abstract class AbstractFormElement extends AbstractNode
                     if (!empty($PA['fieldTSConfig']['suggest.']['default.']['hide'])) {
                         break;
                     }
                     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);
                     $suggestWizard = GeneralUtility::makeInstance(SuggestWizard::class);
-                    $otherWizards[] = $suggestWizard->renderSuggestSelector($PA['itemFormElName'], $table, $field, $row, $PA, $flexFormConfig);
+                    $otherWizards[] = $suggestWizard->renderSuggestSelector($this->data);
                     break;
             }
         }
                     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()
      * @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!
  */
 
  * 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;
 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\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"
 
 /**
  * Generation of TCEform elements of the type "group"
@@ -30,6 +33,11 @@ use TYPO3\CMS\Core\Utility\MathUtility;
 class GroupElement extends AbstractFormElement
 {
     /**
 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.
      *
      * 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()
     {
      */
     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'];
         $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();
 
         $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'][] =
             $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
             // 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 = $this->renderWizards(
-                [$html],
+                [ $html ],
                 $config['wizards'],
                 $table,
                 $row,
                 $fieldName,
                 $parameterArray,
                 $config['wizards'],
                 $table,
                 $row,
                 $fieldName,
                 $parameterArray,
-                $parameterArray['itemFormElName'],
-                $specConf
+                $elementName,
+                BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras'])
             );
         }
             );
         }
+
         $resultArray['html'] = $html;
         return $resultArray;
     }
         $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;
 
 /**
 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
 {
  */
 class ImageManipulationElement extends AbstractFormElement
 {
@@ -168,14 +169,8 @@ class ImageManipulationElement extends AbstractFormElement
     {
         $file = null;
         $fileUid = !empty($row[$fieldName]) ? $row[$fieldName] : null;
     {
         $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 {
         }
         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')) {
                 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')) {
                 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.
  *
 /**
  * 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
 {
  */
 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\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\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
+use TYPO3\CMS\Lang\LanguageService;
 
 /**
  * Render a widget with two boxes side by side.
  *
 
 /**
  * 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
 {
  */
 class SelectMultipleSideBySideElement extends AbstractFormElement
 {
@@ -33,160 +35,226 @@ class SelectMultipleSideBySideElement extends AbstractFormElement
      */
     public function render()
     {
      */
     public function render()
     {
-        $table = $this->data['tableName'];
-        $field = $this->data['fieldName'];
+        $languageService = $this->getLanguageService();
+
         $parameterArray = $this->data['parameterArray'];
         $parameterArray = $this->data['parameterArray'];
-        // Field configuration from TCA:
         $config = $parameterArray['fieldConf']['config'];
         $config = $parameterArray['fieldConf']['config'];
+        $elementName = $parameterArray['itemFormElName'];
 
 
-        $selItems = $config['items'];
-        $html = '';
-        $disabled = '';
         if ($config['readOnly']) {
         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;
                 }
             }
                     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 = [];
         $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;
 
         $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()
      * @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
  *
 /**
  * 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
 {
  */
 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.
  *
  * 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
 {
  */
 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!
  */
 
  * 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.
  *
 /**
  * Render data as a tree.
  *
@@ -77,22 +73,54 @@ class SelectTreeElement extends AbstractFormElement
         $treeWrapperId = 'tree_' . $formElementId;
 
         $fieldName = $this->data['fieldName'];
         $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') {
         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 = [];
         $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-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-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) . '"';
         $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()
     {
      */
     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'] : '';
         $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 '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' => [],
             // 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
 
             // 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
             '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' => '',
 
             // @todo: keys below must be handled / further defined
             'elementBaseName' => '',
-            'flexFormFieldIdentifierPrefix' => 'ID',
             'tabAndInlineStack' => [],
             'inlineData' => [],
             'inlineStructure' => [],
             'tabAndInlineStack' => [],
             'inlineData' => [],
             'inlineStructure' => [],
index f388e6f..1a7d671 100644 (file)
@@ -1170,7 +1170,10 @@ abstract class AbstractItemProvider
         $currentDatabaseValues = array_key_exists($fieldName, $row)
             ? $row[$fieldName]
             : '';
         $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
      *
      * @param mixed $maxItems
      * @return int
+     * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
      */
     public function sanitizeMaxItems($maxItems)
     {
      */
     public function sanitizeMaxItems($maxItems)
     {
+        GeneralUtility::logDeprecatedFunction();
         if (!empty($maxItems)
         if (!empty($maxItems)
-            && (int)$maxItems > 1
+            && (int)$maxItems >= 1
         ) {
             $maxItems = (int)$maxItems;
         } else {
         ) {
             $maxItems = (int)$maxItems;
         } else {
-            $maxItems = 1;
+            $maxItems = 99999;
         }
 
         return $maxItems;
         }
 
         return $maxItems;
index dc8a85d..f614d6f 100644 (file)
@@ -98,11 +98,9 @@ class DatabaseRecordTypeValue implements FormDataProviderInterface
                             1438253614
                         );
                     }
                             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);
                     }
                     // Fetch field of this foreign row from db
                     $foreignRow = $this->getDatabaseRow($foreignTable, $foreignUid, $foreignTableTypeField);
@@ -152,6 +150,7 @@ class DatabaseRecordTypeValue implements FormDataProviderInterface
 
         return $row ?: [];
     }
 
         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
     /**
      * 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) {
 
         $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;
             if (isset($databaseRow[$fieldName])) {
                 $newRow[$fieldName] = $databaseRow[$fieldName];
                 continue;
@@ -55,7 +55,9 @@ class DatabaseRowDefaultValues implements FormDataProviderInterface
                     $newRow[$fieldName] = (string)$fieldConfig['config']['default'];
                 }
             } else {
                     $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'];
             }
         }
                 $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;
         }
         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(
             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
             );
         }
                 1437991120
             );
         }
-        $result['databaseRow']['uid'] = StringUtility::getUniqueId('NEW');
+        if (!isset($result['databaseRow']['uid'])) {
+            $result['databaseRow']['uid'] = StringUtility::getUniqueId('NEW');
+        }
 
         return $result;
     }
 
         return $result;
     }
index 8c30e23..5544c45 100644 (file)
@@ -1,4 +1,5 @@
 <?php
 <?php
+declare(strict_types=1);
 namespace TYPO3\CMS\Backend\Form\FormDataProvider;
 
 /*
 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\Backend\Form\FormDataProviderInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MathUtility;
 
 /**
  * Class EvaluateDisplayConditions implements the TCA 'displayCond' option.
 
 /**
  * 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.
      *
     /**
      * 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
      */
      * @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;
     }
 
     /**
         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) {
         foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) {
+            if ($columnConfiguration['config']['type'] === 'flex') {
+                $flexColumns[$columnName] = $columnConfiguration;
+            }
             if (!isset($columnConfiguration['displayCond'])) {
                 continue;
             }
             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;
     }
 
     /**
         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
      */
      * @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;
     }
 
     /**
         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':
             case 'FIELD':
-                $result = $this->matchFieldCondition($condition, $record, $flexformContext);
-                break;
+                return $this->matchFieldCondition($conditionArray);
             case 'HIDE_FOR_NON_ADMINS':
             case 'HIDE_FOR_NON_ADMINS':
-                $result = $this->matchHideForNonAdminsCondition();
-                break;
+                return (bool)$this->getBackendUser()->isAdmin();
             case 'REC':
             case 'REC':
-                $result = $this->matchRecordCondition($condition, $record);
-                break;
+                return $this->matchRecordCondition($conditionArray);
             case 'VERSION':
             case 'VERSION':
-                $result = $this->matchVersionCondition($condition, $record);
-                break;
+                return $this->matchVersionCondition($conditionArray);
             case 'USER':
             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.
     }
 
     /**
      * 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
      *
      *
      * 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
      */
      * @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);
                 }
         $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;
                     $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);
                 }
                 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) {
                 break;