[!!!][FEATURE] FormEngine element level refactoring 51/51151/74
authorChristian Kuhn <lolli@schwarzbu.ch>
Thu, 5 Jan 2017 17:08:27 +0000 (18:08 +0100)
committerChristian Kuhn <lolli@schwarzbu.ch>
Thu, 26 Jan 2017 16:52:51 +0000 (17:52 +0100)
The patch introduces a new API on FormEngine element level
that substitutes the old "wizards" / renderWizard() API
with a more powerful system.

Single wizards are now split into one of three categories:
* An informational wizard
* A control button / icon
* A true wizard with additonal functionality

Method renderWizards() is still called in elements for compatibility
reasons if people added own scrip/popup/userFunc wizards, but all
core wizards are migrated.

The patch significantly cleans the HTML of single elements, especially
HTML stuff that was added by the SingleFieldContainer is now put down
to single elements, while main HTML wraps formerly done by renderWizards()
is fetched "up" to single elements. This gives single elements full
control about the main HTML it is producing, which is a must have
preparation in order to further advance in this area and to switch
single elements to fluid rendering in one of the next steps.

The patch brings a pretty huge list of TCA changes and
simplifications, all TCA changes are supported by TCA migration,
so existing extensions should benefit out of the box and just
get deprecations logged.

Change-Id: I45083e14e45bbf40c06267b51c9d0b7c15e2f7ab
Resolves: #79440
Resolves: #70032
Releases: master
Reviewed-on: https://review.typo3.org/51151
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: Mona Muzaffar <mona.muzaffar@gmx.de>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
138 files changed:
Build/Resources/Public/Less/TYPO3/_main_form.less
composer.json
composer.lock
typo3/sysext/backend/Classes/Controller/Wizard/ColorpickerController.php
typo3/sysext/backend/Classes/Controller/Wizard/RteController.php [deleted file]
typo3/sysext/backend/Classes/Controller/Wizard/SuggestWizardController.php
typo3/sysext/backend/Classes/Form/AbstractNode.php
typo3/sysext/backend/Classes/Form/Container/AbstractContainer.php
typo3/sysext/backend/Classes/Form/Container/FlexFormContainerContainer.php
typo3/sysext/backend/Classes/Form/Container/FlexFormElementContainer.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/OuterWrapContainer.php
typo3/sysext/backend/Classes/Form/Container/PaletteAndSingleContainer.php
typo3/sysext/backend/Classes/Form/Container/SingleFieldContainer.php
typo3/sysext/backend/Classes/Form/Container/TabsContainer.php
typo3/sysext/backend/Classes/Form/Element/AbstractFormElement.php
typo3/sysext/backend/Classes/Form/Element/CheckboxElement.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/InputDateTimeElement.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Element/InputLinkElement.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Element/InputTextElement.php
typo3/sysext/backend/Classes/Form/Element/NoneElement.php
typo3/sysext/backend/Classes/Form/Element/RadioElement.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/Element/TextElement.php
typo3/sysext/backend/Classes/Form/Element/TextTableElement.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Element/UserElement.php
typo3/sysext/backend/Classes/Form/FieldControl/AddRecord.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FieldControl/EditPopup.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FieldControl/ElementBrowser.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FieldControl/InsertClipboard.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FieldControl/LinkPopup.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FieldControl/ListModule.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FieldControl/ResetSelection.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FieldControl/TableWizard.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FieldWizard/DefaultLanguageDifferences.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FieldWizard/FileThumbnails.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FieldWizard/FileTypeList.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FieldWizard/FileUpload.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FieldWizard/OtherLanguageContent.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FieldWizard/RecordsOverview.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FieldWizard/SelectIcons.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FieldWizard/TableList.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FormDataCompiler.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaText.php
typo3/sysext/backend/Classes/Form/FormResultCompiler.php
typo3/sysext/backend/Classes/Form/NodeExpansion/FieldControl.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/NodeExpansion/FieldInformation.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/NodeExpansion/FieldWizard.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/NodeFactory.php
typo3/sysext/backend/Classes/Form/Utility/FormEngineUtility.php
typo3/sysext/backend/Classes/Form/Wizard/SuggestWizard.php
typo3/sysext/backend/Classes/Form/Wizard/ValueSliderWizard.php
typo3/sysext/backend/Classes/Utility/BackendUtility.php
typo3/sysext/backend/Classes/View/Wizard/Element/BackendLayoutWizardElement.php
typo3/sysext/backend/Configuration/Backend/Routes.php
typo3/sysext/backend/Resources/Private/Templates/OuterWrapContainer.html
typo3/sysext/backend/Resources/Private/Templates/Wizards/SuggestWizard.html
typo3/sysext/backend/Resources/Public/Css/backend.css
typo3/sysext/backend/Resources/Public/JavaScript/ColorPicker.js
typo3/sysext/backend/Resources/Public/JavaScript/FormEngine.js
typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SelectSingleElement.js
typo3/sysext/backend/Resources/Public/JavaScript/FormEngineValidation.js
typo3/sysext/backend/Resources/Public/JavaScript/ValueSlider.js
typo3/sysext/backend/Resources/Public/JavaScript/jsfunc.tbe_editor.js
typo3/sysext/backend/Tests/Unit/Form/Element/AbstractFormElementTest.php [new file with mode: 0644]
typo3/sysext/backend/Tests/Unit/Form/Element/InputDateTimeElementTest.php [new file with mode: 0644]
typo3/sysext/backend/Tests/Unit/Form/Element/InputTextElementTest.php [deleted file]
typo3/sysext/backend/Tests/Unit/Form/Element/NoneElementTest.php [deleted file]
typo3/sysext/backend/Tests/Unit/Form/FormDataCompilerTest.php
typo3/sysext/backend/Tests/Unit/Utility/BackendUtilityTest.php
typo3/sysext/core/Classes/DataHandling/DataHandler.php
typo3/sysext/core/Classes/Database/ReferenceIndex.php
typo3/sysext/core/Classes/Migrations/TcaMigration.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_category.php
typo3/sysext/core/Configuration/TCA/sys_collection.php
typo3/sysext/core/Configuration/TCA/sys_file_collection.php
typo3/sysext/core/Configuration/TCA/sys_file_metadata.php
typo3/sysext/core/Configuration/TCA/sys_file_reference.php
typo3/sysext/core/Configuration/TCA/sys_language.php
typo3/sysext/core/Configuration/TCA/sys_news.php
typo3/sysext/core/Documentation/Changelog/master/Breaking-78899-DroppedFormEngineMethods.rst
typo3/sysext/core/Documentation/Changelog/master/Deprecation-78899-FormEngineMethods.rst
typo3/sysext/core/Documentation/Changelog/master/Deprecation-79440-TcaChanges.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-79440-FormEngineElementExpansion.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Migrations/TcaMigrationTest.php
typo3/sysext/css_styled_content/Configuration/TCA/Overrides/tt_content.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_post.php
typo3/sysext/extensionmanager/Configuration/TCA/tx_extensionmanager_domain_model_extension.php
typo3/sysext/felogin/Configuration/FlexForms/Login.xml
typo3/sysext/felogin/Configuration/TCA/Overrides/fe_groups.php
typo3/sysext/felogin/Configuration/TCA/Overrides/fe_users.php
typo3/sysext/filemetadata/Configuration/TCA/Overrides/sys_file_metadata.php
typo3/sysext/fluid_styled_content/Configuration/TCA/Overrides/tt_content.php
typo3/sysext/form/Configuration/Yaml/FormEngineSetup.yaml
typo3/sysext/frontend/Configuration/TCA/backend_layout.php
typo3/sysext/frontend/Configuration/TCA/fe_groups.php
typo3/sysext/frontend/Configuration/TCA/fe_users.php
typo3/sysext/frontend/Configuration/TCA/pages_language_overlay.php
typo3/sysext/frontend/Configuration/TCA/sys_template.php
typo3/sysext/frontend/Configuration/TCA/tt_content.php
typo3/sysext/frontend/Resources/Private/Language/locallang_tca.xlf
typo3/sysext/frontend/Resources/Private/Language/locallang_ttc.xlf
typo3/sysext/impexp/Tests/Functional/Fixtures/Extensions/impexp_group_files/Configuration/TCA/tx_impexpgroupfiles_item.php
typo3/sysext/indexed_search/Configuration/TCA/index_config.php
typo3/sysext/lang/Resources/Private/Language/locallang_core.xlf
typo3/sysext/rsaauth/Classes/Form/Element/RsaInputElement.php
typo3/sysext/rte_ckeditor/Classes/Form/Element/RichTextElement.php
typo3/sysext/rte_ckeditor/Classes/Form/Resolver/RichTextNodeResolver.php
typo3/sysext/rtehtmlarea/Classes/Controller/Wizard/RteController.php [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Classes/Form/Element/RichTextElement.php
typo3/sysext/rtehtmlarea/Classes/Form/FieldControl/FullScreenRichtext.php [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Classes/Form/Resolver/RichTextNodeResolver.php
typo3/sysext/rtehtmlarea/Configuration/Backend/Routes.php
typo3/sysext/rtehtmlarea/Configuration/TCA/Overrides/sys_news.php [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Configuration/TCA/Overrides/tt_content.php
typo3/sysext/rtehtmlarea/Configuration/TCA/tx_rtehtmlarea_acronym.php
typo3/sysext/rtehtmlarea/Migrations/Code/ClassAliasMap.php [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Resources/Private/Language/locallang.xlf
typo3/sysext/rtehtmlarea/ext_localconf.php
typo3/sysext/sys_action/Configuration/TCA/sys_action.php
typo3/sysext/sys_note/Configuration/TCA/sys_note.php
typo3/sysext/t3editor/Classes/Form/Element/T3editorElement.php
typo3/sysext/t3editor/Configuration/TCA/Overrides/tt_content.php
typo3/sysext/workspaces/Configuration/TCA/sys_workspace.php
typo3/sysext/workspaces/Configuration/TCA/sys_workspace_stage.php

index 054a6ab..19aee9e 100644 (file)
@@ -314,36 +314,26 @@ select {
 // Form Wizards
 //
 .form-wizards-wrap {
-       &.form-wizards-top {
-               > .form-wizards-items {
-                       margin-bottom: 9px;
-               }
+       display: table;
+       width: 100%;
+       > .form-wizards-element {
+               display: table-cell;
+               width: 100%;
        }
-       &.form-wizards-bottom {
-               > .form-wizards-element {
-                       margin-bottom: 9px;
-               }
+       > .form-wizards-items-top {
+               display: table-row;
        }
-       &.form-wizards-aside {
-               display: table;
-               width: 100%;
-               > .form-wizards-element {
-                       width: 100%;
-               }
-               > .form-wizards-element,
-               > .form-wizards-items {
-                       display: table-cell;
-                       vertical-align: top;
-               }
-               > .form-wizards-items {
-                       white-space: nowrap;
-                       padding-left: 5px;
-               }
+       > .form-wizards-items-aside {
+               display: table-cell;
+               vertical-align: top;
+               padding-left: 5px;
+               white-space: nowrap;
        }
-}
-.form-wizards-element {
-       > .table-fit {
-               margin-bottom: 9px;
+       > .form-wizards-items-bottom {
+               display: table-row;
+       }
+       > .form-wizards-items-bottom:first-child {
+               margin-top: 4px;
        }
 }
 
index c0f5da4..2db074c 100644 (file)
@@ -75,6 +75,7 @@
                        "always-add-alias-loader": true,
                        "class-alias-maps": [
                                "typo3/sysext/fluid/Migrations/Code/ClassAliasMap.php",
+                               "typo3/sysext/rtehtmlarea/Migrations/Code/ClassAliasMap.php",
                                "typo3/sysext/version/Migrations/Code/ClassAliasMap.php"
                        ]
                },
index 1685eec..4e8359a 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "2acb42e35d36e35f70942355d0b8e669",
+    "content-hash": "1fe12bb655c3cb1c01ee8d0e5b945e1e",
     "packages": [
         {
             "name": "cogpowered/finediff",
index 9152d2e..67c5b8e 100644 (file)
@@ -24,6 +24,10 @@ use TYPO3\CMS\Core\Utility\PathUtility;
 
 /**
  * Script Class for colorpicker wizard
+ *
+ * Unused with new renderType "inputColorPicker" since v8.
+ *
+ * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
  */
 class ColorpickerController extends AbstractWizardController
 {
@@ -119,9 +123,12 @@ class ColorpickerController extends AbstractWizardController
 
     /**
      * Constructor
+     *
+     * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
      */
     public function __construct()
     {
+        GeneralUtility::logDeprecatedFunction();
         parent::__construct();
         $this->getLanguageService()->includeLLFile('EXT:lang/Resources/Private/Language/locallang_wizards.xlf');
         $GLOBALS['SOBE'] = $this;
diff --git a/typo3/sysext/backend/Classes/Controller/Wizard/RteController.php b/typo3/sysext/backend/Classes/Controller/Wizard/RteController.php
deleted file mode 100644 (file)
index dc8c5b5..0000000
+++ /dev/null
@@ -1,361 +0,0 @@
-<?php
-namespace TYPO3\CMS\Backend\Controller\Wizard;
-
-/*
- * 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\FormEngine;
-use TYPO3\CMS\Backend\Form\FormResultCompiler;
-use TYPO3\CMS\Backend\Form\NodeFactory;
-use TYPO3\CMS\Backend\Template\Components\ButtonBar;
-use TYPO3\CMS\Backend\Utility\BackendUtility;
-use TYPO3\CMS\Core\Database\ConnectionPool;
-use TYPO3\CMS\Core\Imaging\Icon;
-use TYPO3\CMS\Core\Imaging\IconFactory;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-
-/**
- * Script class for rendering the full screen RTE display
- */
-class RteController extends AbstractWizardController
-{
-    /**
-     * Content accumulation for the module.
-     *
-     * @var string
-     */
-    public $content;
-
-    /**
-     * Wizard parameters, coming from FormEngine linking to the wizard.
-     *
-     * @var array
-     */
-    public $P;
-
-    /**
-     * If set, launch a new window with the current records pid.
-     *
-     * @var string
-     */
-    public $popView;
-
-    /**
-     * Set to the URL of this script including variables which is needed to re-display the form. See main()
-     *
-     * @var string
-     */
-    public $R_URI;
-
-    /**
-     * Module configuration
-     *
-     * @var array
-     */
-    public $MCONF = [];
-
-    /**
-     * Constructor
-     */
-    public function __construct()
-    {
-        parent::__construct();
-        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
-        $this->getLanguageService()->includeLLFile('EXT:lang/Resources/Private/Language/locallang_wizards.xlf');
-        $GLOBALS['SOBE'] = $this;
-
-        $this->init();
-    }
-
-    /**
-     * Initialization of the class
-     *
-     * @return void
-     */
-    protected function init()
-    {
-        // Setting GPvars:
-        $this->P = GeneralUtility::_GP('P');
-        $this->popView = GeneralUtility::_GP('popView');
-        $this->R_URI = GeneralUtility::linkThisScript(['popView' => '']);
-        // "Module name":
-        $this->MCONF['name'] = 'wizard_rte';
-        // Need to NOT have the page wrapped in DIV since if we do that we destroy
-        // the feature that the RTE spans the whole height of the page!!!
-    }
-
-    /**
-     * Injects the request object for the current request or subrequest
-     * As this controller goes only through the main() method, it is rather simple for now
-     *
-     * @param ServerRequestInterface $request
-     * @param ResponseInterface $response
-     * @return ResponseInterface
-     */
-    public function mainAction(ServerRequestInterface $request, ResponseInterface $response)
-    {
-        $this->main();
-
-        $response->getBody()->write($this->moduleTemplate->renderContent());
-        return $response;
-    }
-
-    /**
-     * Main function, rendering the document with the iFrame with the RTE in.
-     *
-     * @return void
-     */
-    public function main()
-    {
-        $this->content .= '<form action="'
-            . htmlspecialchars(BackendUtility::getModuleUrl('tce_db'))
-            . '" method="post" enctype="multipart/form-data" id="RteController" name="editform" '
-            . ' onsubmit="return TBE_EDITOR.checkSubmit(1);">';
-        // Translate id to the workspace version:
-        if ($versionedRecord = BackendUtility::getWorkspaceVersionOfRecord(
-            $this->getBackendUserAuthentication()->workspace,
-            $this->P['table'],
-            $this->P['uid'],
-            'uid'
-        )) {
-            $this->P['uid'] = $versionedRecord['uid'];
-        }
-        // If all parameters are available:
-        if ($this->P['table']
-            && $this->P['field']
-            && $this->P['uid']
-            && $this->checkEditAccess($this->P['table'], $this->P['uid'])) {
-            /** @var TcaDatabaseRecord $formDataGroup */
-            $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
-            /** @var FormDataCompiler $formDataCompiler */
-            $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
-            /** @var NodeFactory $nodeFactory */
-            $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
-
-            $formDataCompilerInput = [
-                'vanillaUid' => (int)$this->P['uid'],
-                'tableName' => $this->P['table'],
-                'command' => 'edit',
-                'disabledWizards' => true,
-            ];
-
-            $formData = $formDataCompiler->compile($formDataCompilerInput);
-
-            $formData['fieldListToRender'] = $this->P['field'];
-            $formData['renderType'] = 'outerWrapContainer';
-            $formResult = $nodeFactory->create($formData)->render();
-
-            /** @var FormResultCompiler $formResultCompiler */
-            $formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
-            $formResultCompiler->mergeResult($formResult);
-
-            // override the default jumpToUrl
-            $this->moduleTemplate->addJavaScriptCode(
-                'RteWizardInlineCode',
-                'function jumpToUrl(URL,formEl) {
-                                       if (document.editform) {
-                                               if (!TBE_EDITOR.isFormChanged()) {
-                                                       window.location.href = URL;
-                                               } else if (formEl) {
-                                                       if (formEl.type=="checkbox") formEl.checked = formEl.checked ? 0 : 1;
-                                               }
-                                       } else {
-                                               window.location.href = URL;
-                                       }
-                               }
-                       '
-            );
-
-            // Setting JavaScript of the pid value for viewing:
-            if ($this->popView) {
-                $this->moduleTemplate->addJavaScriptCode(
-                    'PopupViewInlineJS',
-                    BackendUtility::viewOnClick(
-                        $formData['databaseRow']['pid'],
-                        '',
-                        BackendUtility::BEgetRootLine($formData['databaseRow']['pid'])
-                    )
-                );
-            }
-
-            $pageTsConfigMerged = $formData['pageTsConfigMerged'];
-            if ((string)$pageTsConfigMerged['TCEFORM.'][$this->P['table'] . '.'][$this->P['field'] . '.']['RTEfullScreenWidth'] !== '') {
-                $width = (string)$pageTsConfigMerged['TCEFORM.'][$this->P['table'] . '.'][$this->P['field'] . '.']['RTEfullScreenWidth'];
-            } else {
-                $width = '100%';
-            }
-            // Get the form field and wrap it in the table with the buttons:
-            $formContent = $formResult['html'];
-            $formContent = '
-                               <table border="0" cellpadding="0" cellspacing="0" width="' . $width . '" id="typo3-rtewizard">
-                                       <tr>
-                                               <td width="' . $width . '" colspan="2" id="c-formContent">' . $formContent . '</td>
-                                               <td></td>
-                                       </tr>
-                               </table>';
-
-            // Adding hidden fields:
-            $formContent .= '<input type="hidden" name="redirect" value="' . htmlspecialchars($this->R_URI) . '" />
-                                               <input type="hidden" name="_serialNumber" value="' . md5(microtime()) . '" />';
-            // Finally, add the whole setup:
-            $this->content .= $formResultCompiler->addCssFiles()
-                . $formContent
-                . $formResultCompiler->printNeededJSFunctions();
-        } else {
-            // ERROR:
-            $this->content .= '<h2>' . htmlspecialchars($this->getLanguageService()->getLL('forms_title')) . '</h2>'
-                . '<div><span class="text-danger">'
-                . htmlspecialchars($this->getLanguageService()->getLL('table_noData'))
-                . '</span></div>';
-        }
-        // Setting up the buttons and markers for docHeader
-        $this->getButtons();
-        // Build the <body> for the module
-
-        $this->content .= '</form>';
-        $this->moduleTemplate->setContent($this->content);
-    }
-
-    /**
-     * Create the panel of buttons for submitting the form or otherwise perform operations.
-     *
-     * @return array All available buttons as an assoc. array
-     */
-    protected function getButtons()
-    {
-        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
-        if ($this->P['table']
-            && $this->P['field']
-            && $this->P['uid']
-            && $this->checkEditAccess($this->P['table'], $this->P['uid'])) {
-            $closeUrl = GeneralUtility::sanitizeLocalUrl($this->P['returnUrl']);
-            // Getting settings for the undo button:
-            $undoButton = false;
-
-            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
-                ->getQueryBuilderForTable('sys_history');
-
-            $undoButtonR = $queryBuilder
-                ->select('tstamp')
-                ->from('sys_history')
-                ->where(
-                    $queryBuilder->expr()->eq(
-                        'tablename',
-                        $queryBuilder->createNamedParameter($this->P['table'], \PDO::PARAM_STR)
-                    ),
-                    $queryBuilder->expr()->eq(
-                        'recuid',
-                        $queryBuilder->createNamedParameter($this->P['uid'], \PDO::PARAM_INT)
-                    )
-                )
-                ->orderBy('tstamp', 'desc')
-                ->setMaxResults(1)
-                ->execute()
-                ->fetchColumn();
-
-            if ($undoButtonR !== false) {
-                $undoButton = true;
-            }
-            // Close
-            $closeButton = $buttonBar->makeLinkButton()
-                ->setHref($closeUrl)
-                ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
-                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-close', Icon::SIZE_SMALL));
-            $buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 10);
-
-            // Save
-            $saveButton = $buttonBar->makeInputButton()
-                ->setName('_savedok_x')
-                ->setValue('1')
-                ->setForm('RteController')
-                ->setOnClick('TBE_EDITOR.checkAndDoSubmit(1); return false;')
-                ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'))
-                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL));
-            // Save & View
-            $saveAndViewButton = $buttonBar->makeInputButton()
-                ->setName('_savedokview_x')
-                ->setValue('1')
-                ->setForm('RteController')
-                ->setOnClick('document.editform.redirect.value+= ' . GeneralUtility::quoteJSvalue('&popView=1') . '; '
-                    . ' TBE_EDITOR.checkAndDoSubmit(1); return false;')
-                ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.saveDocShow'))
-                ->setIcon(
-                    $this->moduleTemplate->getIconFactory()->getIcon('actions-document-save-view', Icon::SIZE_SMALL)
-                );
-
-            // Save & Close
-            $saveAndCloseButton = $buttonBar->makeInputButton()
-                ->setName('_saveandclosedok_x')
-                ->setValue('1')
-                ->setForm('RteController')
-                ->setOnClick('document.editform.redirect.value=' . GeneralUtility::quoteJSvalue($closeUrl)
-                    . '; TBE_EDITOR.checkAndDoSubmit(1); return false;')
-                ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.saveCloseDoc'))
-                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
-                    'actions-document-save-close',
-                    Icon::SIZE_SMALL
-                ));
-
-            // Save SplitButton
-            $saveSplitButton = $buttonBar->makeSplitButton()
-                ->addItem($saveButton)
-                ->addItem($saveAndViewButton)
-                ->addItem($saveAndCloseButton);
-            $buttonBar->addButton($saveSplitButton, ButtonBar::BUTTON_POSITION_LEFT, 20);
-
-            // Undo/Revert:
-            if ($undoButton) {
-                $aOnClick = 'window.location.href=' .
-                    GeneralUtility::quoteJSvalue(
-                        BackendUtility::getModuleUrl(
-                            'record_history',
-                            [
-                                'element' => $this->P['table'] . ':' . $this->P['uid'],
-                                'revert' => 'field:' . $this->P['field'],
-                                'returnUrl' => $this->R_URI,
-                            ]
-                        )
-                    ) . '; return false;';
-
-                $undoText = $this->getLanguageService()->sL(
-                    'LLL:EXT:lang/Resources/Private/Language/locallang_wizards.xlf:rte_undoLastChange'
-                );
-                $lastChangeLabel = sprintf(
-                    $undoText,
-                    BackendUtility::calcAge(
-                        ($GLOBALS['EXEC_TIME'] - $undoButtonR['tstamp']),
-                        $this->getLanguageService()->sL(
-                            'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears'
-                        )
-                    )
-                );
-
-                $undoRevertButton = $buttonBar->makeLinkButton()
-                    ->setHref('#')
-                    ->setOnClick($aOnClick)
-                    ->setTitle($lastChangeLabel)
-                    ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-edit-undo', Icon::SIZE_SMALL));
-                $buttonBar->addButton($undoRevertButton, ButtonBar::BUTTON_POSITION_LEFT, 30);
-            }
-            // Shortcut
-            $shortButton = $buttonBar->makeShortcutButton()
-                ->setModuleName($this->MCONF['name'])
-                ->setGetVariables(['P']);
-            $buttonBar->addButton($shortButton);
-        }
-    }
-}
index 0047a31..10ae2c4 100644 (file)
@@ -97,7 +97,7 @@ class SuggestWizardController
 
         $pageTsConfig = BackendUtility::getPagesTSconfig($pid);
 
-        $wizardConfig = $fieldConfig['wizards']['suggest'];
+        $wizardConfig = $fieldConfig['suggestOptions'] ?? [];
 
         $queryTables = $this->getTablesToQueryFromFieldConfiguration($fieldConfig);
         $whereClause = $this->getWhereClause($fieldConfig);
index 124f653..a4116aa 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+declare(strict_types=1);
 namespace TYPO3\CMS\Backend\Form;
 
 /*
@@ -23,6 +24,13 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 abstract class AbstractNode implements NodeInterface
 {
     /**
+     * Instance of the node factory to create sub elements, container and single element expansions.
+     *
+     * @var NodeFactory
+     */
+    protected $nodeFactory;
+
+    /**
      * Main data array to work on, given from parent to child elements
      *
      * @var array
@@ -30,13 +38,30 @@ abstract class AbstractNode implements NodeInterface
     protected $data = [];
 
     /**
-     * Set data to data array.
+     * A list of default field information added to the element / container.
      *
-     * @todo: Should NOT set the nodeFactory instance, this is done by AbstractContainer only,
-     * @todo: but not done for Element classes: Elements are tree leaves, they MUST
-     * @todo: not create new nodes again.
-     * @todo: Currently, AbstractFormElement still does that, but do not rely on the fact that
-     * @todo: Element classes have an instance of NodeFactory at hand.
+     * @var array
+     */
+    protected $defaultFieldInformation = [];
+
+    /**
+     * A list of default field controls added to the element / container.
+     * This property is often reset by single elements.
+     *
+     * @var array
+     */
+    protected $defaultFieldControl = [];
+
+    /**
+     * A list of default field wizards added to the element / container.
+     * This property is often reset by single elements.
+     *
+     * @var array
+     */
+    protected $defaultFieldWizard = [];
+
+    /**
+     * Set data to data array and register node factory to render sub elements
      *
      * @param NodeFactory $nodeFactory
      * @param array $data
@@ -44,6 +69,7 @@ abstract class AbstractNode implements NodeInterface
     public function __construct(NodeFactory $nodeFactory, array $data)
     {
         $this->data = $data;
+        $this->nodeFactory = $nodeFactory;
     }
 
     /**
@@ -60,7 +86,7 @@ abstract class AbstractNode implements NodeInterface
      *
      * @return array
      */
-    protected function initializeResultArray()
+    protected function initializeResultArray(): array
     {
         return [
             'additionalJavaScriptPost' => [],
@@ -68,7 +94,9 @@ abstract class AbstractNode implements NodeInterface
             'additionalHiddenFields' => [],
             'additionalInlineLanguageLabelFiles' => [],
             'stylesheetFiles' => [],
-            // can hold strings or arrays, string = requireJS module, array = requireJS module + callback e.g. array('TYPO3/Foo/Bar', 'function() {}')
+            // can hold strings or arrays,
+            // string = requireJS module,
+            // array = requireJS module + callback e.g. array('TYPO3/Foo/Bar', 'function() {}')
             'requireJsModules' => [],
             'inlineData' => [],
             'html' => '',
@@ -80,14 +108,17 @@ abstract class AbstractNode implements NodeInterface
      *
      * @param array $existing Currently merged array
      * @param array $childReturn Array returned by child
+     * @param bool $mergeHtml If false, the ['html'] section of $childReturn will NOT be added to $existing
      * @return array Result array
      */
-    protected function mergeChildReturnIntoExistingResult(array $existing, array $childReturn)
+    protected function mergeChildReturnIntoExistingResult(array $existing, array $childReturn, bool $mergeHtml = true): array
     {
-        if (!empty($childReturn['html'])) {
+        if ($mergeHtml && !empty($childReturn['html'])) {
             $existing['html'] .= LF . $childReturn['html'];
         }
         if (!empty($childReturn['extJSCODE'])) {
+            // @deprecated since TYPO3 CMS 8, will be removed in TYPO3 CMS 9.
+            GeneralUtility::logDeprecatedFunction();
             $existing['extJSCODE'] .= LF . $childReturn['extJSCODE'];
         }
         foreach ($childReturn['additionalJavaScriptPost'] as $value) {
@@ -127,9 +158,11 @@ abstract class AbstractNode implements NodeInterface
      *
      * @param array $config
      * @return string
+     * @deprecated since TYPO3 v8, will be removed in TYPO3 v9 - use getValidationDataAsJsonString() instead
      */
-    protected function getValidationDataAsDataAttribute(array $config)
+    protected function getValidationDataAsDataAttribute(array $config): string
     {
+        GeneralUtility::logDeprecatedFunction();
         return sprintf(' data-formengine-validation-rules="%s" ', htmlspecialchars($this->getValidationDataAsJsonString($config)));
     }
 
@@ -139,24 +172,28 @@ abstract class AbstractNode implements NodeInterface
      * @param array $config
      * @return string
      */
-    protected function getValidationDataAsJsonString(array $config)
+    protected function getValidationDataAsJsonString(array $config): string
     {
         $validationRules = [];
         if (!empty($config['eval'])) {
             $evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
-            unset($config['eval']);
             foreach ($evalList as $evalType) {
                 $validationRules[] = [
                     'type' => $evalType,
-                    'config' => $config
                 ];
             }
         }
         if (!empty($config['range'])) {
-            $validationRules[] = [
+            $newValidationRule = [
                 'type' => 'range',
-                'config' => $config['range']
             ];
+            if (!empty($config['range']['lower'])) {
+                $newValidationRule['lower'] = $config['range']['lower'];
+            }
+            if (!empty($config['range']['upper'])) {
+                $newValidationRule['upper'] = $config['range']['upper'];
+            }
+            $validationRules[] = $newValidationRule;
         }
         if (!empty($config['maxitems']) || !empty($config['minitems'])) {
             $minItems = (isset($config['minitems'])) ? (int)$config['minitems'] : 0;
index b105f1d..4109b50 100644 (file)
@@ -15,10 +15,7 @@ namespace TYPO3\CMS\Backend\Form\Container;
  */
 
 use TYPO3\CMS\Backend\Form\AbstractNode;
-use TYPO3\CMS\Backend\Form\NodeFactory;
-use TYPO3\CMS\Backend\Utility\BackendUtility;
-use TYPO3\CMS\Core\Imaging\Icon;
-use TYPO3\CMS\Core\Imaging\IconFactory;
+use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
@@ -29,30 +26,61 @@ use TYPO3\CMS\Fluid\View\StandaloneView;
 abstract class AbstractContainer extends AbstractNode
 {
     /**
-     * Instance of the node factory to create sub elements and container.
+     * Merge field information configuration with default and render them.
      *
-     * @var NodeFactory
+     * @return array Result array
      */
-    protected $nodeFactory;
+    protected function renderFieldInformation(): array
+    {
+        $options = $this->data;
+        $fieldInformation = $this->defaultFieldInformation;
+        $currentRenderType = $this->data['renderType'];
+        $fieldInformationFromTca = $options['processedTca']['ctrl']['container'][$currentRenderType]['fieldInformation'] ?? [];
+        ArrayUtility::mergeRecursiveWithOverrule($fieldInformation, $fieldInformationFromTca);
+        $options['renderType'] = 'fieldInformation';
+        $options['renderData']['fieldInformation'] = $fieldInformation;
+        return $this->nodeFactory->create($options)->render();
+    }
+
+    /**
+     * Merge field control configuration with default controls and render them.
+     *
+     * @return array Result array
+     */
+    protected function renderFieldControl(): array
+    {
+        $options = $this->data;
+        $fieldControl = $this->defaultFieldControl;
+        $currentRenderType = $this->data['renderType'];
+        $fieldControlFromTca = $options['processedTca']['ctrl']['container'][$currentRenderType]['fieldControl'] ?? [];
+        ArrayUtility::mergeRecursiveWithOverrule($fieldControl, $fieldControlFromTca);
+        $options['renderType'] = 'fieldControl';
+        $options['renderData']['fieldControl'] = $fieldControl;
+        return $this->nodeFactory->create($options)->render();
+    }
 
     /**
-     * Container objects give $nodeFactory down to other containers.
+     * Merge field wizard configuration with default wizards and render them.
      *
-     * @param NodeFactory $nodeFactory
-     * @param array $data
+     * @return array Result array
      */
-    public function __construct(NodeFactory $nodeFactory, array $data)
+    protected function renderFieldWizard(): array
     {
-        parent::__construct($nodeFactory, $data);
-        $this->nodeFactory = $nodeFactory;
+        $options = $this->data;
+        $fieldWizard = $this->defaultFieldWizard;
+        $currentRenderType = $this->data['renderType'];
+        $fieldWizardFromTca = $options['processedTca']['ctrl']['container'][$currentRenderType]['fieldWizard'] ?? [];
+        ArrayUtility::mergeRecursiveWithOverrule($fieldWizard, $fieldWizardFromTca);
+        $options['renderType'] = 'fieldWizard';
+        $options['renderData']['fieldWizard'] = $fieldWizard;
+        return $this->nodeFactory->create($options)->render();
     }
 
     /**
-     * A single field of TCA 'types' 'showitem' can have four semicolon separated configuration options:
+     * A single field of TCA 'types' 'showitem' can have three semicolon separated configuration options:
      *   fieldName: Name of the field to be found in TCA 'columns' section
      *   fieldLabel: An alternative field label
      *   paletteName: Name of a palette to be found in TCA 'palettes' section that is rendered after this field
-     *   extra: Special configuration options of this field
      *
      * @param string $field Semicolon separated field configuration
      * @throws \RuntimeException
@@ -96,57 +124,4 @@ abstract class AbstractContainer extends AbstractNode
         ]);
         return $view->render();
     }
-
-    /**
-     * Rendering preview output of a field value which is not shown as a form field but just outputted.
-     *
-     * @param string $value The value to output
-     * @param array $config Configuration for field.
-     * @param string $field Name of field.
-     * @return string HTML formatted output
-     */
-    protected function previewFieldValue($value, $config, $field = '')
-    {
-        if ($config['config']['type'] === 'group' && ($config['config']['internal_type'] === 'file' || $config['config']['internal_type'] === 'file_reference')) {
-            // Ignore upload folder if internal_type is file_reference
-            if ($config['config']['internal_type'] === 'file_reference') {
-                $config['config']['uploadfolder'] = '';
-            }
-            $table = 'tt_content';
-            // Making the array of file items:
-            $itemArray = GeneralUtility::trimExplode(',', $value, true);
-            // Showing thumbnails:
-            $imgs = [];
-            $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
-            foreach ($itemArray as $imgRead) {
-                $imgParts = explode('|', $imgRead);
-                $imgPath = rawurldecode($imgParts[0]);
-                $rowCopy = [];
-                $rowCopy[$field] = $imgPath;
-                // Icon + click menu:
-                $absFilePath = GeneralUtility::getFileAbsFileName($config['config']['uploadfolder'] ? $config['config']['uploadfolder'] . '/' . $imgPath : $imgPath);
-                $fileInformation = pathinfo($imgPath);
-                $title = $fileInformation['basename'] . ($absFilePath && @is_file($absFilePath))
-                    ? ' (' . GeneralUtility::formatSize(filesize($absFilePath)) . ')'
-                    : ' - FILE NOT FOUND!';
-                $fileIcon = '<span title="' . htmlspecialchars($title) . '">' . $iconFactory->getIconForFileExtension($fileInformation['extension'], Icon::SIZE_SMALL)->render() . '</span>';
-                $imgs[] =
-                    '<span class="text-nowrap">' .
-                    BackendUtility::thumbCode(
-                        $rowCopy,
-                        $table,
-                        $field,
-                        '',
-                        '',
-                        $config['config']['uploadfolder'], 0, ' align="middle"'
-                    ) .
-                    ($absFilePath ? BackendUtility::wrapClickMenuOnIcon($fileIcon, $absFilePath, 0, 1, '', '+copy,info,edit,view') : $fileIcon) .
-                    $imgPath .
-                    '</span>';
-            }
-            return implode('<br />', $imgs);
-        } else {
-            return nl2br(htmlspecialchars($value));
-        }
-    }
 }
index 7b2f04b..9000efa 100644 (file)
@@ -113,10 +113,9 @@ class FlexFormContainerContainer extends AbstractContainer
         $html[] =    '</div>';
         $html[] = '</div>';
 
-        $containerContentResult['html'] = '';
         $resultArray = $this->initializeResultArray();
         $resultArray['html'] = implode(LF, $html);
-        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $containerContentResult);
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $containerContentResult, false);
 
         return $resultArray;
     }
index 774fed1..69ddac5 100644 (file)
@@ -75,7 +75,6 @@ class FlexFormElementContainer extends AbstractContainer
                         'label' => $languageService->sL(trim($flexFormFieldArray['label'])),
                         'config' => $flexFormFieldArray['config'],
                         'children' => $flexFormFieldArray['children'],
-                        'defaultExtras' => $flexFormFieldArray['defaultExtras'],
                         'onChange' => $flexFormFieldArray['onChange'],
                     ],
                     'fieldChangeFunc' => $parameterArray['fieldChangeFunc'],
@@ -152,8 +151,7 @@ class FlexFormElementContainer extends AbstractContainer
                 $html[] = '</div>';
 
                 $resultArray['html'] .= implode(LF, $html);
-                $childResult['html'] = '';
-                $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult);
+                $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, false);
             }
         }
 
index b937ae8..726d349 100644 (file)
@@ -91,8 +91,7 @@ class FlexFormTabsContainer extends AbstractContainer
                 'linkTitle' => trim($sheetDataStructure['ROOT']['sheetShortDescr']) ? $languageService->sL(trim($sheetDataStructure['ROOT']['sheetShortDescr'])) : '',
             ];
 
-            $childReturn['html'] = '';
-            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childReturn);
+            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childReturn, false);
         }
 
         $resultArray['html'] = $this->renderTabMenu($tabElements, $domIdPrefix);
index fa24fa8..b4e40b3 100644 (file)
@@ -285,8 +285,7 @@ class InlineControlContainer extends AbstractContainer
             $options['renderType'] = 'inlineRecordContainer';
             $childResult = $this->nodeFactory->create($options)->render();
             $html .= $childResult['html'];
-            $childArray['html'] = '';
-            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult);
+            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, false);
             if (!$options['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
                 // Don't add record to list of "valid" uids if it is only the default
                 // language record of a not yet localized child
@@ -323,7 +322,7 @@ class InlineControlContainer extends AbstractContainer
 
         // Publish the uids of the child records in the given order to the browser
         $html .= '<input type="hidden" name="' . $nameForm . '" value="' . implode(',', $sortableRecordUids) . '" '
-            . $this->getValidationDataAsDataAttribute(['type' => 'inline', 'minitems' => $config['minitems'], 'maxitems' => $config['maxitems']])
+            . ' data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString(['type' => 'inline', 'minitems' => $config['minitems'], 'maxitems' => $config['maxitems']])) . '"'
             . ' class="inlineRecord" />';
         // Close the wrap for all inline fields (container)
         $html .= '</div>';
index c057670..3981ba4 100644 (file)
@@ -138,13 +138,11 @@ class InlineRecordContainer extends AbstractContainer
                 if (isset($data['combinationChild'])) {
                     $combinationChild = $this->renderCombinationChild($data, $appendFormFieldNames);
                     $combinationHtml = $combinationChild['html'];
-                    $combinationChild['html'] = '';
-                    $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $combinationChild);
+                    $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $combinationChild, false);
                 }
                 $childArray = $this->renderChild($data);
                 $html = $childArray['html'];
-                $childArray['html'] = '';
-                $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
+                $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray, false);
             } else {
                 // This string is the marker for the JS-function to check if the full content has already been loaded
                 $html = '<!--notloaded-->';
index 05d5048..ca5a066 100644 (file)
@@ -129,8 +129,18 @@ class OuterWrapContainer extends AbstractContainer
             ]);
         }
 
+        $fieldInformationResult = $this->renderFieldInformation();
+        $fieldInformationHtml = $fieldInformationResult['html'];
+        $result = $this->mergeChildReturnIntoExistingResult($result, $fieldInformationResult, false);
+
+        $fieldWizardResult = $this->renderfieldWizard();
+        $fieldWizardHtml = $fieldWizardResult['html'];
+        $result = $this->mergeChildReturnIntoExistingResult($result, $fieldWizardResult, false);
+
         $view->assignMultiple([
             'pageTitle' => $pageTitle,
+            'fieldInformationHtml' => $fieldInformationHtml,
+            'fieldWizardHtml' => $fieldWizardHtml,
             'childHtml' => $childHtml,
             'icon' => $icon,
             'tableTitle' => $tableTitle,
index b59532b..daa4ab3 100644 (file)
@@ -149,8 +149,7 @@ class PaletteAndSingleContainer extends AbstractContainer
                     ];
                 }
 
-                $childResultArray['html'] = '';
-                $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $childResultArray);
+                $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $childResultArray, false);
             }
         }
 
@@ -226,9 +225,8 @@ class PaletteAndSingleContainer extends AbstractContainer
                         'fieldLabel' => $fieldLabel,
                         'fieldHtml' => $singleFieldContentArray['html'],
                     ];
-                    $singleFieldContentArray['html'] = '';
                 }
-                $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $singleFieldContentArray);
+                $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $singleFieldContentArray, false);
             }
         }
 
index ed517c5..5a0d95a 100644 (file)
@@ -16,12 +16,8 @@ namespace TYPO3\CMS\Backend\Form\Container;
 
 use TYPO3\CMS\Backend\Form\InlineStackProcessor;
 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
-use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
-use TYPO3\CMS\Core\Imaging\Icon;
-use TYPO3\CMS\Core\Imaging\IconFactory;
 use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
-use TYPO3\CMS\Core\Utility\DiffUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Lang\LanguageService;
@@ -32,8 +28,7 @@ use TYPO3\CMS\Lang\LanguageService;
  * This container is the last one in the chain before processing is handed over to single element classes.
  * If a single field is of type flex or inline, it however creates FlexFormEntryContainer or InlineControlContainer.
  *
- * The container does various checks and processing for a given single fields, for example it resolves
- * display conditions and the HTML to compare different languages.
+ * The container does various checks and processing for a given single fields.
  */
 class SingleFieldContainer extends AbstractContainer
 {
@@ -46,18 +41,12 @@ class SingleFieldContainer extends AbstractContainer
     public function render()
     {
         $backendUser = $this->getBackendUserAuthentication();
-        $languageService = $this->getLanguageService();
         $resultArray = $this->initializeResultArray();
 
         $table = $this->data['tableName'];
         $row = $this->data['databaseRow'];
         $fieldName = $this->data['fieldName'];
 
-        // @todo: it should be safe at this point, this array exists ...
-        if (!is_array($this->data['processedTca']['columns'][$fieldName])) {
-            return $resultArray;
-        }
-
         $parameterArray = [];
         $parameterArray['fieldConf'] = $this->data['processedTca']['columns'][$fieldName];
 
@@ -152,14 +141,18 @@ class SingleFieldContainer extends AbstractContainer
 
         // JavaScript code for event handlers:
         $parameterArray['fieldChangeFunc'] = [];
-        $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = 'TBE_EDITOR.fieldChanged(' . GeneralUtility::quoteJSvalue($table) . ',' . GeneralUtility::quoteJSvalue($row['uid']) . ',' . GeneralUtility::quoteJSvalue($fieldName) . ',' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ');';
+        $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = 'TBE_EDITOR.fieldChanged('
+                . GeneralUtility::quoteJSvalue($table) . ','
+                . GeneralUtility::quoteJSvalue($row['uid']) . ','
+                . GeneralUtility::quoteJSvalue($fieldName) . ','
+                . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName'])
+            . ');';
         if ($alertMsgOnChange) {
             $parameterArray['fieldChangeFunc']['alert'] = $alertMsgOnChange;
         }
 
         // If this is the child of an inline type and it is the field creating the label
         if ($this->isInlineChildAndLabelField($table, $fieldName)) {
-            /** @var InlineStackProcessor $inlineStackProcessor */
             $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
             $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
             $inlineDomObjectId = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
@@ -171,7 +164,10 @@ class SingleFieldContainer extends AbstractContainer
                     $row['uid']
                 ]
             );
-            $parameterArray['fieldChangeFunc']['inline'] = 'inline.handleChangedField(' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ',' . GeneralUtility::quoteJSvalue($inlineObjectId) . ');';
+            $parameterArray['fieldChangeFunc']['inline'] = 'inline.handleChangedField('
+                    . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ','
+                    . GeneralUtility::quoteJSvalue($inlineObjectId)
+                . ');';
         }
 
         // Based on the type of the item, call a render function on a child element
@@ -185,253 +181,10 @@ class SingleFieldContainer extends AbstractContainer
             $options['renderType'] = $parameterArray['fieldConf']['config']['type'];
         }
         $resultArray = $this->nodeFactory->create($options)->render();
-
-        // If output is empty stop further processing.
-        // This means there was internal processing only and we don't need to add additional information
-        if (empty($resultArray['html'])) {
-            return $resultArray;
-        }
-
-        $html = $resultArray['html'];
-
-        // @todo: the language handling, the null and the placeholder stuff should be embedded in the single
-        // @todo: element classes. Basically, this method should return here and have the element classes
-        // @todo: decide on language stuff and other wraps already.
-
-        // Add language + diff
-        $renderLanguageDiff = true;
-        if ($parameterArray['fieldConf']['l10n_display'] && (GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'hideDiff')
-            || GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'defaultAsReadonly'))
-        ) {
-            $renderLanguageDiff = false;
-        }
-        if ($renderLanguageDiff) {
-            $html = $this->renderDefaultLanguageContent($table, $fieldName, $row, $html);
-            $html = $this->renderDefaultLanguageDiff($table, $fieldName, $row, $html);
-        }
-
-        $fieldItemClasses = [
-            't3js-formengine-field-item'
-        ];
-
-        // NULL value and placeholder handling
-        $nullControlNameAttribute = ' name="' . htmlspecialchars('control[active][' . $table . '][' . $row['uid'] . '][' . $fieldName . ']') . '"';
-        if (!empty($parameterArray['fieldConf']['config']['eval']) && GeneralUtility::inList($parameterArray['fieldConf']['config']['eval'], 'null')
-            && (empty($parameterArray['fieldConf']['config']['mode']) || $parameterArray['fieldConf']['config']['mode'] !== 'useOrOverridePlaceholder')
-        ) {
-            // This field has eval=null set, but has no useOverridePlaceholder defined.
-            // Goal is to have a field that can distinct between NULL and empty string in the database.
-            // A checkbox and an additional hidden field will be created, both with the same name
-            // and prefixed with "control[active]". If the checkbox is set (value 1), the value from the casual
-            // input field will be written to the database. If the checkbox is not set, the hidden field
-            // transfers value=0 to DataHandler, the value of the input field will then be reset to NULL by the
-            // DataHandler at an early point in processing, so NULL will be written to DB as field value.
-
-            // If the value of the field *is* NULL at the moment, an additional class is set
-            // @todo: This does not work well at the moment, but is kept for now. see input_14 of ext:styleguide as example
-            $checked = ' checked="checked"';
-            if ($this->data['databaseRow'][$fieldName] === null) {
-                $fieldItemClasses[] = 'disabled';
-                $checked = '';
-            }
-
-            $formElementName = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
-            $onChange = htmlspecialchars(
-                'typo3form.fieldSetNull(' . GeneralUtility::quoteJSvalue($formElementName) . ', !this.checked)'
-            );
-
-            $nullValueWrap = [];
-            $nullValueWrap[] = '<div class="' . implode(' ', $fieldItemClasses) . '">';
-            $nullValueWrap[] =    '<div class="t3-form-field-disable"></div>';
-            $nullValueWrap[] =    '<div class="checkbox t3-form-field-eval-null-checkbox">';
-            $nullValueWrap[] =        '<label>';
-            $nullValueWrap[] =            '<input type="hidden"' . $nullControlNameAttribute . ' value="0" />';
-            $nullValueWrap[] =            '<input type="checkbox"' . $nullControlNameAttribute . ' value="1" onchange="' . $onChange . '"' . $checked . ' /> &nbsp;';
-            $nullValueWrap[] =        '</label>';
-            $nullValueWrap[] =    '</div>';
-            $nullValueWrap[] =    $html;
-            $nullValueWrap[] = '</div>';
-
-            $html = implode(LF, $nullValueWrap);
-        } elseif (isset($parameterArray['fieldConf']['config']['mode']) && $parameterArray['fieldConf']['config']['mode'] === 'useOrOverridePlaceholder') {
-            // This field has useOverridePlaceholder set.
-            // Here, a value from a deeper DB structure can be "fetched up" as value, and can also be overridden by a
-            // local value. This is used in FAL, where eg. the "title" field can have the default value from sys_file_metadata,
-            // the title field of sys_file_reference is then set to NULL. Or the "override" checkbox is set, and a string
-            // or an empty string is then written to the field of sys_file_reference.
-            // The situation is similar to the NULL handling above, but additionally a "default" value should be shown.
-            // To achieve this, again a hidden control[hidden] field is added together with a checkbox with the same name
-            // to transfer the information whether the default value should be used or not: Checkbox checked transfers 1 as
-            // value in control[active], meaning the overridden value should be used.
-            // Additionally to the casual input field, a second field is added containing the "placeholder" value. This
-            // field has no name attribute and is not transferred at all. Those two are then hidden / shown depending
-            // on the state of the above checkbox in via JS.
-
-            $placeholder = empty($parameterArray['fieldConf']['config']['placeholder']) ? '' : $parameterArray['fieldConf']['config']['placeholder'];
-            $onChange = 'typo3form.fieldTogglePlaceholder(' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ', !this.checked)';
-            $checked = $parameterArray['itemFormElValue'] === null ? '' : ' checked="checked"';
-            $disabled = '';
-            $fallbackValue = 0;
-            if (strlen(BackendUtility::getRecordTitlePrep($placeholder, 20)) > 0) {
-                $overrideLabel = sprintf($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'), BackendUtility::getRecordTitlePrep($placeholder, 20));
-            } else {
-                $fallbackValue = 1;
-                $checked = ' checked="checked"';
-                $disabled = ' disabled="disabled"';
-                $overrideLabel = sprintf($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'), BackendUtility::getRecordTitlePrep($placeholder, 20));
-            }
-
-            $resultArray['additionalJavaScriptPost'][] = 'typo3form.fieldTogglePlaceholder('
-                . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ', ' . ($checked ? 'false' : 'true') . ');';
-
-            // Renders an input or textarea field depending on type of "parent"
-            $options = [];
-            $options['databaseRow'] = [];
-            $options['table'] = '';
-            $options['parameterArray'] = $parameterArray;
-            $options['parameterArray']['itemFormElValue'] = GeneralUtility::fixed_lgd_cs($placeholder, 30);
-            $options['renderType'] = 'none';
-            $noneElementResult = $this->nodeFactory->create($options)->render();
-            $noneElementHtml = $noneElementResult['html'];
-
-            $placeholderWrap = [];
-            $placeholderWrap[] = '<div class="' . implode(' ', $fieldItemClasses) . '">';
-            $placeholderWrap[] =    '<div class="t3-form-field-disable"></div>';
-            $placeholderWrap[] =    '<div class="checkbox">';
-            $placeholderWrap[] =        '<label>';
-            $placeholderWrap[] =            '<input type="hidden"' . $nullControlNameAttribute . ' value="' . $fallbackValue . '" />';
-            $placeholderWrap[] =            '<input type="checkbox"' . $nullControlNameAttribute . ' value="1" id="tce-forms-textfield-use-override-' . $fieldName . '-' . $row['uid'] . '" onchange="' . htmlspecialchars($onChange) . '"' . $checked . $disabled . ' />';
-            $placeholderWrap[] =            $overrideLabel;
-            $placeholderWrap[] =        '</label>';
-            $placeholderWrap[] =    '</div>';
-            $placeholderWrap[] =    '<div class="t3js-formengine-placeholder-placeholder">';
-            $placeholderWrap[] =        $noneElementHtml;
-            $placeholderWrap[] =    '</div>';
-            $placeholderWrap[] =    '<div class="t3js-formengine-placeholder-formfield">';
-            $placeholderWrap[] =        $html;
-            $placeholderWrap[] =    '</div>';
-            $placeholderWrap[] = '</div>';
-
-            $html = implode(LF, $placeholderWrap);
-        } elseif ($parameterArray['fieldConf']['config']['type'] !== 'user' || empty($parameterArray['fieldConf']['config']['noTableWrapping'])) {
-            // Add a casual wrap if the field is not of type user with no wrap requested.
-            $standardWrap = [];
-            $standardWrap[] = '<div class="' . implode(' ', $fieldItemClasses) . '">';
-            $standardWrap[] =    '<div class="t3-form-field-disable"></div>';
-            $standardWrap[] =    $html;
-            $standardWrap[] = '</div>';
-
-            $html = implode(LF, $standardWrap);
-        }
-
-        $resultArray['html'] = $html;
         return $resultArray;
     }
 
     /**
-     * Renders the display of default language record content around current field.
-     * Will render content if any is found in the internal array.
-     *
-     * @param string $table Table name of the record being edited
-     * @param string $field Field name represented by $item
-     * @param array $row Record array of the record being edited
-     * @param string $item HTML of the form field. This is what we add the content to.
-     * @return string Item string returned again, possibly with the original value added to.
-     */
-    protected function renderDefaultLanguageContent($table, $field, $row, $item)
-    {
-        if (is_array($this->data['defaultLanguageRow'])) {
-            $defaultLanguageValue = BackendUtility::getProcessedValue(
-                $table,
-                $field,
-                $this->data['defaultLanguageRow'][$field],
-                0,
-                true,
-                false,
-                $this->data['defaultLanguageRow']['uid'],
-                true,
-                $this->data['defaultLanguageRow']['pid']
-            );
-            $fieldConfig = $this->data['processedTca']['columns'][$field];
-            // Don't show content if it's for IRRE child records:
-            if ($fieldConfig['config']['type'] !== 'inline' && $fieldConfig['config']['type'] !== 'flex') {
-                /** @var IconFactory $iconFactory */
-                $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
-                if ($defaultLanguageValue !== '') {
-                    $item .= '<div class="t3-form-original-language">'
-                        . $iconFactory->getIcon($this->data['systemLanguageRows'][0]['flagIconIdentifier'], Icon::SIZE_SMALL)->render()
-                        . $this->previewFieldValue($defaultLanguageValue, $fieldConfig, $field) . '</div>';
-                }
-                $additionalPreviewLanguages = $this->data['additionalLanguageRows'];
-                foreach ($additionalPreviewLanguages as $previewLanguage) {
-                    $defaultLanguageValue = BackendUtility::getProcessedValue(
-                        $table,
-                        $field,
-                        $previewLanguage[$field],
-                        0,
-                        true
-                    );
-                    if ($defaultLanguageValue !== '') {
-                        $item .= '<div class="t3-form-original-language">'
-                            . $iconFactory->getIcon($this->data['systemLanguageRows'][$previewLanguage['sys_language_uid']]['flagIconIdentifier'], Icon::SIZE_SMALL)->render()
-                            . $this->previewFieldValue($defaultLanguageValue, $fieldConfig, $field) . '</div>';
-                    }
-                }
-            }
-        }
-        return $item;
-    }
-
-    /**
-     * Renders the diff-view of default language record content compared with what the record was originally translated from.
-     * Will render content if any is found in the internal array
-     *
-     * @param string $table Table name of the record being edited
-     * @param string $field Field name represented by $item
-     * @param array $row Record array of the record being edited
-     * @param string  $item HTML of the form field. This is what we add the content to.
-     * @return string Item string returned again, possibly with the original value added to.
-     */
-    protected function renderDefaultLanguageDiff($table, $field, $row, $item)
-    {
-        if (is_array($this->data['defaultLanguageDiffRow'][$table . ':' . $row['uid']])) {
-            // Initialize:
-            $dLVal = [
-                'old' => $this->data['defaultLanguageDiffRow'][$table . ':' . $row['uid']],
-                'new' => $this->data['defaultLanguageRow']
-            ];
-            // There must be diff-data:
-            if (isset($dLVal['old'][$field])) {
-                if ((string)$dLVal['old'][$field] !== (string)$dLVal['new'][$field]) {
-                    // Create diff-result:
-                    /** @var DiffUtility $diffUtility */
-                    $diffUtility = GeneralUtility::makeInstance(DiffUtility::class);
-                    $diffUtility->stripTags = false;
-                    $diffres = $diffUtility->makeDiffDisplay(
-                        BackendUtility::getProcessedValue($table, $field, $dLVal['old'][$field], 0, 1),
-                        BackendUtility::getProcessedValue($table, $field, $dLVal['new'][$field], 0, 1)
-                    );
-                    $item .= '
-                                               <div class="t3-form-original-language-diff">
-                                                       <div class="t3-form-original-language-diffheader">'
-                                . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.changeInOrig'))
-                            . '</div>
-                                                       <div class="t3-form-original-language-diffcontent">
-                                                               <div class="diff">
-                                                                       <div class="diff-item">
-                                                                               <div class="diff-item-result diff-item-result-inline">' . $diffres . '</div>
-                                                                       </div>
-                                                               </div>
-                                                       </div>
-                                               </div>
-                                       ';
-                }
-            }
-        }
-        return $item;
-    }
-
-    /**
      * Checks if the $table is the child of an inline type AND the $field is the label field of this table.
      * This function is used to dynamically update the label while editing. This has no effect on labels,
      * that were processed by a FormEngine-hook on saving.
index acf5e46..5ac2b67 100644 (file)
@@ -89,8 +89,7 @@ class TabsContainer extends AbstractContainer
                 'label' => $tabWithLabelAndElements['label'],
                 'content' => $childArray['html'],
             ];
-            $childArray['html'] = '';
-            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
+            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray, false);
         }
 
         $resultArray['html'] = $this->renderTabMenu($tabElements, $domIdPrefix);
index 883ed97..8483abb 100644 (file)
@@ -15,18 +15,13 @@ namespace TYPO3\CMS\Backend\Form\Element;
  */
 
 use TYPO3\CMS\Backend\Form\AbstractNode;
-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\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\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\Lang\LanguageService;
 
 /**
@@ -56,9 +51,9 @@ abstract class AbstractFormElement extends AbstractNode
     protected $maxInputWidth = 50;
 
     /**
-     * @var NodeFactory
+     * @var IconFactory
      */
-    protected $nodeFactory;
+    protected $iconFactory;
 
     /**
      * Container objects give $nodeFactory down to other containers.
@@ -70,16 +65,231 @@ abstract class AbstractFormElement extends AbstractNode
     {
         parent::__construct($nodeFactory, $data);
         $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
-        // @todo: this must vanish as soon as elements are clean
-        $this->nodeFactory = $nodeFactory;
     }
 
     /**
-     * @return bool TRUE if wizards are disabled on a global level
+     * Merge field information configuration with default and render them.
+     *
+     * @return array Result array
      */
-    protected function isWizardsDisabled()
+    protected function renderFieldInformation(): array
     {
-        return !empty($this->data['disabledWizards']);
+        $options = $this->data;
+        $fieldInformation = $this->defaultFieldInformation;
+        $fieldInformationFromTca = $options['parameterArray']['fieldConf']['config']['fieldInformation'] ?? [];
+        ArrayUtility::mergeRecursiveWithOverrule($fieldInformation, $fieldInformationFromTca);
+        $options['renderType'] = 'fieldInformation';
+        $options['renderData']['fieldInformation'] = $fieldInformation;
+        return $this->nodeFactory->create($options)->render();
+    }
+
+    /**
+     * Merge field control configuration with default controls and render them.
+     *
+     * @return array Result array
+     */
+    protected function renderFieldControl(): array
+    {
+        $options = $this->data;
+        $fieldControl = $this->defaultFieldControl;
+        $fieldControlFromTca = $options['parameterArray']['fieldConf']['config']['fieldControl'] ?? [];
+        ArrayUtility::mergeRecursiveWithOverrule($fieldControl, $fieldControlFromTca);
+        $options['renderType'] = 'fieldControl';
+        $options['renderData']['fieldControl'] = $fieldControl;
+        return $this->nodeFactory->create($options)->render();
+    }
+
+    /**
+     * Merge field wizard configuration with default wizards and render them.
+     *
+     * @return array Result array
+     */
+    protected function renderFieldWizard(): array
+    {
+        $options = $this->data;
+        $fieldWizard = $this->defaultFieldWizard;
+        $fieldWizardFromTca = $options['parameterArray']['fieldConf']['config']['fieldWizard'] ?? [];
+        ArrayUtility::mergeRecursiveWithOverrule($fieldWizard, $fieldWizardFromTca);
+        $options['renderType'] = 'fieldWizard';
+        $options['renderData']['fieldWizard'] = $fieldWizard;
+        return $this->nodeFactory->create($options)->render();
+    }
+
+    /**
+     * Returns true if the "null value" checkbox should be rendered. This is used in some
+     * "text" based types like "text" and "input" for some renderType's.
+     *
+     * A field has eval=null set, but has no useOverridePlaceholder defined.
+     * Goal is to have a field that can distinct between NULL and empty string in the database.
+     * A checkbox and an additional hidden field will be created, both with the same name
+     * and prefixed with "control[active]". If the checkbox is set (value 1), the value from the casual
+     * input field will be written to the database. If the checkbox is not set, the hidden field
+     * transfers value=0 to DataHandler, the value of the input field will then be reset to NULL by the
+     * DataHandler at an early point in processing, so NULL will be written to DB as field value.
+     *
+     * All that only works if the field is not within flex form scope since flex forms
+     * can not store a "null" value or distinct it from "empty string".
+     *
+     * @return bool
+     */
+    protected function hasNullCheckboxButNoPlaceholder(): bool
+    {
+        $hasNullCheckboxNoPlaceholder = false;
+        $parameterArray = $this->data['parameterArray'];
+        $mode = $parameterArray['fieldConf']['config']['mode'] ?? '';
+        if (empty($this->data['flexFormDataStructureIdentifier'])
+            && !empty($parameterArray['fieldConf']['config']['eval'])
+            && GeneralUtility::inList($parameterArray['fieldConf']['config']['eval'], 'null')
+            && ($mode !== 'useOrOverridePlaceholder')
+        ) {
+            $hasNullCheckboxNoPlaceholder = true;
+        }
+        return $hasNullCheckboxNoPlaceholder;
+    }
+
+    /**
+     * Returns true if the "null value" checkbox should be rendered and the placeholder
+     * handling is enabled. This is used in some "text" based types like "text" and
+     * "input" for some renderType's.
+     *
+     * A field has useOverridePlaceholder set and null in eval and is not within a flex form.
+     * Here, a value from a deeper DB structure can be "fetched up" as value, and can also be overridden by a
+     * local value. This is used in FAL, where eg. the "title" field can have the default value from sys_file_metadata,
+     * the title field of sys_file_reference is then set to NULL. Or the "override" checkbox is set, and a string
+     * or an empty string is then written to the field of sys_file_reference.
+     * The situation is similar to hasNullCheckboxButNoPlaceholder(), but additionally a "default" value should be shown.
+     * To achieve this, again a hidden control[hidden] field is added together with a checkbox with the same name
+     * to transfer the information whether the default value should be used or not: Checkbox checked transfers 1 as
+     * value in control[active], meaning the overridden value should be used.
+     * Additionally to the casual input field, a second field is added containing the "placeholder" value. This
+     * field has no name attribute and is not transferred at all. Those two are then hidden / shown depending
+     * on the state of the above checkbox in via JS.
+     *
+     * @return bool
+     */
+    protected function hasNullCheckboxWithPlaceholder(): bool
+    {
+        $hasNullCheckboxWithPlaceholder = false;
+        $parameterArray = $this->data['parameterArray'];
+        $mode = $parameterArray['fieldConf']['config']['mode'] ?? '';
+        if (empty($this->data['flexFormDataStructureIdentifier'])
+            && !empty($parameterArray['fieldConf']['config']['eval'])
+            && GeneralUtility::inList($parameterArray['fieldConf']['config']['eval'], 'null')
+            && ($mode === 'useOrOverridePlaceholder')
+        ) {
+            $hasNullCheckboxWithPlaceholder = true;
+        }
+        return $hasNullCheckboxWithPlaceholder;
+    }
+
+    /**
+     * Format field content if 'format' is set to date, filesize, ..., user
+     *
+     * @param string $format Configuration for the display.
+     * @param string $itemValue The value to display
+     * @param array $formatOptions Format options
+     * @return string Formatted field value
+     */
+    protected function formatValue($format, $itemValue, $formatOptions = [])
+    {
+        switch ($format) {
+            case 'date':
+                if ($itemValue) {
+                    $option = isset($formatOptions['option']) ? trim($formatOptions['option']) : '';
+                    if ($option) {
+                        if (isset($formatOptions['strftime']) && $formatOptions['strftime']) {
+                            $value = strftime($option, $itemValue);
+                        } else {
+                            $value = date($option, $itemValue);
+                        }
+                    } else {
+                        $value = date('d-m-Y', $itemValue);
+                    }
+                } else {
+                    $value = '';
+                }
+                if (isset($formatOptions['appendAge']) && $formatOptions['appendAge']) {
+                    $age = BackendUtility::calcAge(
+                        $GLOBALS['EXEC_TIME'] - $itemValue,
+                        $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
+                    );
+                    $value .= ' (' . $age . ')';
+                }
+                $itemValue = $value;
+                break;
+            case 'datetime':
+                // compatibility with "eval" (type "input")
+                if ($itemValue !== '' && !is_null($itemValue)) {
+                    $itemValue = date('H:i d-m-Y', (int)$itemValue);
+                }
+                break;
+            case 'time':
+                // compatibility with "eval" (type "input")
+                if ($itemValue !== '' && !is_null($itemValue)) {
+                    $itemValue = date('H:i', (int)$itemValue);
+                }
+                break;
+            case 'timesec':
+                // compatibility with "eval" (type "input")
+                if ($itemValue !== '' && !is_null($itemValue)) {
+                    $itemValue = date('H:i:s', (int)$itemValue);
+                }
+                break;
+            case 'year':
+                // compatibility with "eval" (type "input")
+                if ($itemValue !== '' && !is_null($itemValue)) {
+                    $itemValue = date('Y', (int)$itemValue);
+                }
+                break;
+            case 'int':
+                $baseArr = ['dec' => 'd', 'hex' => 'x', 'HEX' => 'X', 'oct' => 'o', 'bin' => 'b'];
+                $base = isset($formatOptions['base']) ? trim($formatOptions['base']) : '';
+                $format = isset($baseArr[$base]) ? $baseArr[$base] : 'd';
+                $itemValue = sprintf('%' . $format, $itemValue);
+                break;
+            case 'float':
+                // default precision
+                $precision = 2;
+                if (isset($formatOptions['precision'])) {
+                    $precision = MathUtility::forceIntegerInRange($formatOptions['precision'], 1, 10, $precision);
+                }
+                $itemValue = sprintf('%.' . $precision . 'f', $itemValue);
+                break;
+            case 'number':
+                $format = isset($formatOptions['option']) ? trim($formatOptions['option']) : '';
+                $itemValue = sprintf('%' . $format, $itemValue);
+                break;
+            case 'md5':
+                $itemValue = md5($itemValue);
+                break;
+            case 'filesize':
+                // We need to cast to int here, otherwise empty values result in empty output,
+                // but we expect zero.
+                $value = GeneralUtility::formatSize((int)$itemValue);
+                if (!empty($formatOptions['appendByteSize'])) {
+                    $value .= ' (' . $itemValue . ')';
+                }
+                $itemValue = $value;
+                break;
+            case 'user':
+                $func = trim($formatOptions['userFunc']);
+                if ($func) {
+                    $params = [
+                        'value' => $itemValue,
+                        'args' => $formatOptions['userFunc'],
+                        'config' => [
+                            'type' => 'none',
+                            'format' => $format,
+                            'format.' => $formatOptions,
+                        ],
+                    ];
+                    $itemValue = GeneralUtility::callUserFunction($func, $params, $this);
+                }
+                break;
+            default:
+                // Do nothing e.g. when $format === ''
+        }
+        return $itemValue;
     }
 
     /**
@@ -98,39 +308,101 @@ abstract class AbstractFormElement extends AbstractNode
     }
 
     /**
-     * @var IconFactory
+     * @return bool TRUE if wizards are disabled on a global level
+     * @deprecated since TYPO3 v8, will be removed in TYPO3 v9 - remove together with renderWizards(), log is thrown in renderWizards()
      */
-    protected $iconFactory;
+    protected function isWizardsDisabled()
+    {
+        return !empty($this->data['disabledWizards']);
+    }
 
     /**
      * Rendering wizards for form fields.
      *
-     * @param array $itemKinds Array with the real item in the first value
+     * Deprecated, old "wizard" API. This method will be removed in v9, but is kept for
+     * backwards compatibility. Extensions that give the item HTML in $itemKinds, trigger
+     * the legacy mode of this method which wraps calculated wizards around the given item HTML.
+     *
+     * This method is deprecated and will vanish in v9. Migrate old wizards to the "fieldWizard",
+     * "fieldInformation" and "fieldControl" API instead.
+     *
+     * @param null|array $itemKinds Array with the real item in the first value. Array in legacy mode, else null
      * @param array $wizConf The "wizards" key from the config array for the field (from TCA)
      * @param string $table Table name
      * @param array $row The record array
-     * @param string $field The field name
+     * @param string $fieldName The field name
      * @param array $PA Additional configuration array.
      * @param string $itemName The field name
      * @param array $specConf Special configuration if available.
      * @param bool $RTE Whether the RTE could have been loaded.
-     *
-     * @return string The new item value.
+     * @return string|array String in legacy mode, an array with the buttons and the controls in non-legacy mode
      * @throws \InvalidArgumentException
+     * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
      */
-    protected function renderWizards($itemKinds, $wizConf, $table, $row, $field, $PA, $itemName, $specConf, $RTE = false)
-    {
+    protected function renderWizards(
+        $itemKinds = null,
+        $wizConf = null,
+        $table = null,
+        $row = null,
+        $fieldName = null,
+        $PA = null,
+        $itemName = null,
+        $specConf = null,
+        $RTE = null
+    ) {
+        if ($itemKinds !== null) {
+            // Deprecation log if the old $itemsKinds array comes in containing the item HTML - all core elements
+            // deliver null here. If not null, the legacy mode of the method is enabled that wraps the calculated
+            // wizards around given item HTML.
+            GeneralUtility::logDeprecatedFunction();
+        }
+        $item = '';
+        if (is_array($itemKinds)) {
+            $item = $itemKinds[0];
+        }
+
+        if ($wizConf === null) {
+            $wizConf = $this->data['parameterArray']['fieldConf']['config']['wizards'] ?? null;
+        }
+        if ($table === null) {
+            $table = $this->data['tableName'];
+        }
+        if ($row === null) {
+            $row = $this->data['databaseRow'];
+        }
+        if ($fieldName === null) {
+            $fieldName = $this->data['fieldName'];
+        }
+        if ($PA === null) {
+            $PA = $this->data['parameterArray'];
+        }
+        if ($itemName === null) {
+            $itemName = $PA['itemFormElName'];
+        }
+        if ($RTE === null) {
+            $RTE = false;
+            if ((bool)$this->data['parameterArray']['fieldConf']['config']['enableRichtext'] === true) {
+                $RTE = true;
+            }
+        }
+
         // Return not changed main item directly if wizards are disabled
         if (!is_array($wizConf) || $this->isWizardsDisabled()) {
-            return $itemKinds[0];
+            if ($itemKinds === null) {
+                return [
+                    'fieldControl' => [],
+                    'fieldWizard' => [],
+                ];
+            } else {
+                return $item;
+            }
         }
 
         $languageService = $this->getLanguageService();
 
         $fieldChangeFunc = $PA['fieldChangeFunc'];
-        $item = $itemKinds[0];
         $md5ID = 'ID' . GeneralUtility::shortMD5($itemName);
-        $prefixOfFormElName = 'data[' . $table . '][' . $row['uid'] . '][' . $field . ']';
+        $prefixOfFormElName = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
         $flexFormPath = '';
         if (GeneralUtility::isFirstPartOfStr($PA['itemFormElName'], $prefixOfFormElName)) {
             $flexFormPath = str_replace('][', '/', substr($PA['itemFormElName'], strlen($prefixOfFormElName) + 1, -1));
@@ -141,9 +413,6 @@ abstract class AbstractFormElement extends AbstractNode
             $itemName .= '[]';
         }
 
-        // Contains wizard identifiers enabled for this record type, see "special configuration" docs
-        $wizardsEnabledByType = $specConf['wizards']['parameters'];
-
         $buttonWizards = [];
         $otherWizards = [];
         foreach ($wizConf as $wizardIdentifier => $wizardConfiguration) {
@@ -163,13 +432,6 @@ abstract class AbstractFormElement extends AbstractNode
             // Wizards can be shown based on selected "type" of record. If this is the case, the wizard configuration
             // is set to enableByTypeConfig = 1, and the wizardIdentifier is found in $wizardsEnabledByType
             $wizardIsEnabled = true;
-            if (
-                isset($wizardConfiguration['enableByTypeConfig'])
-                && (bool)$wizardConfiguration['enableByTypeConfig']
-                && (!is_array($wizardsEnabledByType) || !in_array($wizardIdentifier, $wizardsEnabledByType))
-            ) {
-                $wizardIsEnabled = false;
-            }
             // Disable if wizard is for RTE fields only and the handled field is no RTE field or RTE can not be loaded
             if (isset($wizardConfiguration['RTEonly']) && (bool)$wizardConfiguration['RTEonly'] && !$RTE) {
                 $wizardIsEnabled = false;
@@ -197,13 +459,14 @@ abstract class AbstractFormElement extends AbstractNode
 
             switch ($wizardConfiguration['type']) {
                 case 'userFunc':
+                    GeneralUtility::logDeprecatedFunction();
                     $params = [];
                     $params['params'] = $wizardConfiguration['params'];
                     $params['exampleImg'] = $wizardConfiguration['exampleImg'];
                     $params['table'] = $table;
                     $params['uid'] = $row['uid'];
                     $params['pid'] = $row['pid'];
-                    $params['field'] = $field;
+                    $params['field'] = $fieldName;
                     $params['flexFormPath'] = $flexFormPath;
                     $params['md5ID'] = $md5ID;
                     $params['returnUrl'] = $this->data['returnUrl'];
@@ -214,7 +477,7 @@ abstract class AbstractFormElement extends AbstractNode
                     $params['fieldChangeFunc'] = $fieldChangeFunc;
                     $params['fieldChangeFuncHash'] = GeneralUtility::hmac(serialize($fieldChangeFunc));
 
-                    $params['item'] = &$item;
+                    $params['item'] = $item;
                     $params['icon'] = $icon;
                     $params['iTitle'] = $iTitle;
                     $params['wConf'] = $wizardConfiguration;
@@ -223,13 +486,14 @@ abstract class AbstractFormElement extends AbstractNode
                     break;
 
                 case 'script':
+                    GeneralUtility::logDeprecatedFunction();
                     $params = [];
                     $params['params'] = $wizardConfiguration['params'];
                     $params['exampleImg'] = $wizardConfiguration['exampleImg'];
                     $params['table'] = $table;
                     $params['uid'] = $row['uid'];
                     $params['pid'] = $row['pid'];
-                    $params['field'] = $field;
+                    $params['field'] = $fieldName;
                     $params['flexFormPath'] = $flexFormPath;
                     $params['md5ID'] = $md5ID;
                     $params['returnUrl'] = $this->data['returnUrl'];
@@ -239,7 +503,7 @@ abstract class AbstractFormElement extends AbstractNode
                     if (isset($wizardConfiguration['module']['urlParameters']) && is_array($wizardConfiguration['module']['urlParameters'])) {
                         $urlParameters = $wizardConfiguration['module']['urlParameters'];
                     }
-                    $wScript = BackendUtility::getModuleUrl($wizardConfiguration['module']['name'], $urlParameters, '');
+                    $wScript = BackendUtility::getModuleUrl($wizardConfiguration['module']['name'], $urlParameters);
                     $url = $wScript . (strstr($wScript, '?') ? '' : '?') . GeneralUtility::implodeArrayForUrl('', ['P' => $params]);
                     $buttonWizards[] =
                         '<a class="btn btn-default" href="' . htmlspecialchars($url) . '" onclick="this.blur(); return !TBE_EDITOR.isFormChanged();">'
@@ -248,13 +512,14 @@ abstract class AbstractFormElement extends AbstractNode
                     break;
 
                 case 'popup':
+                    GeneralUtility::logDeprecatedFunction();
                     $params = [];
                     $params['params'] = $wizardConfiguration['params'];
                     $params['exampleImg'] = $wizardConfiguration['exampleImg'];
                     $params['table'] = $table;
                     $params['uid'] = $row['uid'];
                     $params['pid'] = $row['pid'];
-                    $params['field'] = $field;
+                    $params['field'] = $fieldName;
                     $params['flexFormPath'] = $flexFormPath;
                     $params['md5ID'] = $md5ID;
                     $params['returnUrl'] = $this->data['returnUrl'];
@@ -270,7 +535,7 @@ abstract class AbstractFormElement extends AbstractNode
                     if (isset($wizardConfiguration['module']['urlParameters']) && is_array($wizardConfiguration['module']['urlParameters'])) {
                         $urlParameters = $wizardConfiguration['module']['urlParameters'];
                     }
-                    $wScript = BackendUtility::getModuleUrl($wizardConfiguration['module']['name'], $urlParameters, '');
+                    $wScript = BackendUtility::getModuleUrl($wizardConfiguration['module']['name'], $urlParameters);
                     $url = $wScript . (strstr($wScript, '?') ? '' : '?') . GeneralUtility::implodeArrayForUrl('', ['P' => $params]);
 
                     $onlyIfSelectedJS = '';
@@ -300,89 +565,18 @@ abstract class AbstractFormElement extends AbstractNode
                             $icon .
                         '</a>';
                     break;
-
-                case 'slider':
-                    $params = [];
-                    $params['fieldConfig'] = $PA['fieldConf']['config'];
-                    $params['field'] = $field;
-                    $params['table'] = $table;
-                    $params['flexFormPath'] = $flexFormPath;
-                    $params['md5ID'] = $md5ID;
-                    $params['itemName'] = $itemName;
-                    $params['wConf'] = $wizardConfiguration;
-                    $params['row'] = $row;
-
-                    /** @var ValueSliderWizard $wizard */
-                    $wizard = GeneralUtility::makeInstance(ValueSliderWizard::class);
-                    $otherWizards[] = $wizard->renderWizard($params);
-                    break;
-
-                case 'select':
-                    // The select wizard is a select drop down added to the main element. It provides all the functionality
-                    // that select items can do for us, so we process this element via data processing.
-                    // @todo: This should be embedded in an own provider called in the main data group to not handle this on the fly here
-
-                    // Select wizard page TS can be set in TCEFORM."table"."field".wizards."wizardName"
-                    $pageTsConfig = [];
-                    if (isset($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$field . '.']['wizards.'][$wizardIdentifier . '.'])
-                        && is_array($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$field . '.']['wizards.'][$wizardIdentifier . '.'])
-                    ) {
-                        $pageTsConfig['TCEFORM.']['dummySelectWizard.'][$wizardIdentifier . '.'] = $this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$field . '.']['wizards.'][$wizardIdentifier . '.'];
-                    }
-                    $selectWizardDataInput = [
-                        'tableName' => 'dummySelectWizard',
-                        'command' => 'edit',
-                        'pageTsConfig' => $pageTsConfig,
-                        'processedTca' => [
-                            'ctrl' => [],
-                            'columns' => [
-                                $wizardIdentifier => [
-                                    'type' => 'select',
-                                    'renderType' => 'selectSingle',
-                                    'config' => $wizardConfiguration,
-                                ],
-                            ],
-                        ],
-                    ];
-                    /** @var OnTheFly $formDataGroup */
-                    $formDataGroup = GeneralUtility::makeInstance(OnTheFly::class);
-                    $formDataGroup->setProviderList([ TcaSelectItems::class ]);
-                    /** @var FormDataCompiler $formDataCompiler */
-                    $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
-                    $compilerResult = $formDataCompiler->compile($selectWizardDataInput);
-                    $selectWizardItems = $compilerResult['processedTca']['columns'][$wizardIdentifier]['config']['items'];
-
-                    $options = [];
-                    $options[] = '<option>' . $iTitle . '</option>';
-                    foreach ($selectWizardItems as $selectWizardItem) {
-                        $options[] = '<option value="' . htmlspecialchars($selectWizardItem[1]) . '">' . htmlspecialchars($selectWizardItem[0]) . '</option>';
-                    }
-                    if ($wizardConfiguration['mode'] == 'append') {
-                        $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0].value=\'\'+this.options[this.selectedIndex].value+document.editform[' . GeneralUtility::quoteJSvalue($itemName) . '].value';
-                    } elseif ($wizardConfiguration['mode'] == 'prepend') {
-                        $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0].value+=\'\'+this.options[this.selectedIndex].value';
-                    } else {
-                        $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0].value=this.options[this.selectedIndex].value';
-                    }
-                    $otherWizards[] =
-                        '<select' .
-                            ' id="' . StringUtility::getUniqueId('tceforms-select-') . '"' .
-                            ' class="form-control tceforms-select tceforms-wizardselect"' .
-                            ' onchange="' . htmlspecialchars($assignValue . ';this.blur();this.selectedIndex=0;' . implode('', $fieldChangeFunc)) . '"' .
-                        '>' .
-                            implode('', $options) .
-                        '</select>';
-                    break;
-                case 'suggest':
-                    if (!empty($PA['fieldTSConfig']['suggest.']['default.']['hide'])) {
-                        break;
-                    }
-                    $suggestWizard = GeneralUtility::makeInstance(SuggestWizard::class);
-                    $otherWizards[] = $suggestWizard->renderSuggestSelector($this->data);
-                    break;
             }
         }
 
+        if ($itemKinds === null) {
+            // Return an array with the two wizard types directly if the legacy mode
+            // is not enabled.
+            return [
+                'fieldControl' => $buttonWizards,
+                'fieldWizard' => $otherWizards,
+            ];
+        }
+
         // For each rendered wizard, put them together around the item.
         if (!empty($buttonWizards) || !empty($otherWizards)) {
             $innerContent = '';
@@ -392,22 +586,17 @@ abstract class AbstractFormElement extends AbstractNode
             $innerContent .= implode(' ', $otherWizards);
 
             // Position
-            $classes = ['form-wizards-wrap'];
             if ($wizConf['_POSITION'] === 'left') {
-                $classes[] = 'form-wizards-aside';
-                $innerContent = '<div class="form-wizards-items">' . $innerContent . '</div><div class="form-wizards-element">' . $item . '</div>';
+                $innerContent = '<div class="form-wizards-items-aside">' . $innerContent . '</div><div class="form-wizards-element">' . $item . '</div>';
             } elseif ($wizConf['_POSITION'] === 'top') {
-                $classes[] = 'form-wizards-top';
-                $innerContent = '<div class="form-wizards-items">' . $innerContent . '</div><div class="form-wizards-element">' . $item . '</div>';
+                $innerContent = '<div class="form-wizards-items-top">' . $innerContent . '</div><div class="form-wizards-element">' . $item . '</div>';
             } elseif ($wizConf['_POSITION'] === 'bottom') {
-                $classes[] = 'form-wizards-bottom';
-                $innerContent = '<div class="form-wizards-element">' . $item . '</div><div class="form-wizards-items">' . $innerContent . '</div>';
+                $innerContent = '<div class="form-wizards-element">' . $item . '</div><div class="form-wizards-items-bottom">' . $innerContent . '</div>';
             } else {
-                $classes[] = 'form-wizards-aside';
-                $innerContent = '<div class="form-wizards-element">' . $item . '</div><div class="form-wizards-items">' . $innerContent . '</div>';
+                $innerContent = '<div class="form-wizards-element">' . $item . '</div><div class="form-wizards-items-aside">' . $innerContent . '</div>';
             }
             $item = '
-                               <div class="' . implode(' ', $classes) . '">
+                               <div class="form-wizards-wrap">
                                        ' . $innerContent . '
                                </div>';
         }
index 62350bb..913ef29 100644 (file)
@@ -22,13 +22,32 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 class CheckboxElement extends AbstractFormElement
 {
     /**
+     * Default field wizards enabled for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldWizard = [
+        'otherLanguageContent' => [
+            'renderType' => 'otherLanguageContent',
+        ],
+        'defaultLanguageDifferences' => [
+            'renderType' => 'defaultLanguageDifferences',
+            'after' => [
+                'otherLanguageContent',
+            ],
+        ],
+    ];
+
+    /**
      * This will render a checkbox or an array of checkboxes
      *
      * @return array As defined in initializeResultArray() of AbstractNode
      */
     public function render()
     {
-        $html = '';
+        $resultArray = $this->initializeResultArray();
+
+        $elementHtml = '';
         $disabled = false;
         if ($this->data['parameterArray']['fieldConf']['config']['readOnly']) {
             $disabled = true;
@@ -71,12 +90,12 @@ class CheckboxElement extends AbstractFormElement
                     6 => 'visible-lg-block'
                 ];
             }
-            $html .= '<div class="checkbox-row row">';
+            $elementHtml .= '<div class="checkbox-row row">';
             $counter = 0;
             // @todo: figure out in which cases checkbox items to not begin at 0 and why and when this would be useful
             foreach ($items as $itemKey => $itemDefinition) {
                 $label = $itemDefinition[0];
-                $html .=
+                $elementHtml .=
                     '<div class="checkbox-column ' . $colClass . '">'
                         . $this->renderSingleCheckboxElement($label, $itemKey, $formElementValue, $numberOfItems, $this->data['parameterArray'], $disabled) .
                     '</div>';
@@ -84,25 +103,50 @@ class CheckboxElement extends AbstractFormElement
                 if ($counter < $numberOfItems && !empty($colClear)) {
                     foreach ($colClear as $rowBreakAfter => $clearClass) {
                         if ($counter % $rowBreakAfter === 0) {
-                            $html .= '<div class="clearfix ' . $clearClass . '"></div>';
+                            $elementHtml .= '<div class="clearfix ' . $clearClass . '"></div>';
                         }
                     }
                 }
             }
-            $html .= '</div>';
+            $elementHtml .= '</div>';
         } else {
             $counter = 0;
             foreach ($items as $itemKey => $itemDefinition) {
                 $label = $itemDefinition[0];
-                $html .=  $this->renderSingleCheckboxElement($label, $counter, $formElementValue, $numberOfItems, $this->data['parameterArray'], $disabled);
+                $elementHtml .=  $this->renderSingleCheckboxElement($label, $counter, $formElementValue, $numberOfItems, $this->data['parameterArray'], $disabled);
                 $counter = $counter + 1;
             }
         }
         if (!$disabled) {
-            $html .= '<input type="hidden" name="' . $this->data['parameterArray']['itemFormElName'] . '" value="' . htmlspecialchars($formElementValue) . '" />';
+            $elementHtml .= '<input type="hidden" name="' . $this->data['parameterArray']['itemFormElName'] . '" value="' . htmlspecialchars($formElementValue) . '" />';
         }
-        $resultArray = $this->initializeResultArray();
-        $resultArray['html'] = $html;
+
+        $fieldInformationResult = $this->renderFieldInformation();
+        $fieldInformationHtml = $fieldInformationResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
+
+        $fieldWizardResult = $this->renderFieldWizard();
+        $fieldWizardHtml = $fieldWizardResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
+
+        $html = [];
+        $html[] = '<div class="t3js-formengine-field-item">';
+        if (!$disabled) {
+            $html[] = $fieldInformationHtml;
+        }
+        $html[] =   '<div class="form-wizards-wrap">';
+        $html[] =       '<div class="form-wizards-element">';
+        $html[] =           $elementHtml;
+        $html[] =       '</div>';
+        if (!$disabled) {
+            $html[] =   '<div class="form-wizards-items-bottom">';
+            $html[] =       $fieldWizardHtml;
+            $html[] =   '</div>';
+        }
+        $html[] =   '</div>';
+        $html[] = '</div>';
+
+        $resultArray['html'] = implode(LF, $html);
         return $resultArray;
     }
 
index 217f30b..5ba7510 100644 (file)
@@ -14,28 +14,83 @@ namespace TYPO3\CMS\Backend\Form\Element;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Backend\Clipboard\Clipboard;
 use TYPO3\CMS\Backend\Form\InlineStackProcessor;
-use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Imaging\Icon;
-use TYPO3\CMS\Core\Messaging\FlashMessage;
-use TYPO3\CMS\Core\Messaging\FlashMessageService;
-use TYPO3\CMS\Core\Resource\ProcessedFile;
-use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
+use TYPO3\CMS\Lang\LanguageService;
 
 /**
- * Generation of TCEform elements of the type "group"
+ * Generation of elements of the type "group"
  */
 class GroupElement extends AbstractFormElement
 {
     /**
-     * @var Clipboard
+     * Default field controls for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldControl = [
+        'elementBrowser' => [
+            'renderType' => 'elementBrowser',
+        ],
+        'insertClipboard' => [
+            'renderType' => 'insertClipboard',
+            'after' => [ 'elementBrowser' ],
+        ],
+        'editPopup' => [
+            'renderType' => 'editPopup',
+            'disabled' => true,
+            'after' => [ 'insertClipboard' ],
+        ],
+        'addRecord' => [
+            'renderType' => 'addRecord',
+            'disabled' => true,
+            'after' => [ 'editPopup' ],
+        ],
+        'listModule' => [
+            'renderType' => 'listModule',
+            'disabled' => true,
+            'after' => [ 'addRecord' ],
+        ],
+    ];
+
+    /**
+     * Default field wizards for this element
+     *
+     * @var array
      */
-    protected $clipboard;
+    protected $defaultFieldWizard = [
+        'tableList' => [
+            'renderType' => 'tableList',
+        ],
+        'fileTypeList' => [
+            'renderType' => 'fileTypeList',
+            'after' => [ 'tableList' ],
+        ],
+        'fileThumbnails' => [
+            'renderType' => 'fileThumbnails',
+            'after' => [ 'fileTypeList' ],
+        ],
+        'recordsOverview' => [
+            'renderType' => 'recordsOverview',
+            'after' => [ 'fileThumbnails' ],
+        ],
+        'fileUpload' => [
+            'renderType' => 'fileUpload',
+            'after' => [ 'recordsOverview' ],
+        ],
+        'otherLanguageContent' => [
+            'renderType' => 'otherLanguageContent',
+            'after' => [ 'fileUpload' ],
+        ],
+        'defaultLanguageDifferences' => [
+            'renderType' => 'defaultLanguageDifferences',
+            'after' => [ 'otherLanguageContent' ],
+        ],
+    ];
 
     /**
      * This will render a selector box into which elements from either
@@ -48,6 +103,7 @@ class GroupElement extends AbstractFormElement
     {
         $languageService = $this->getLanguageService();
         $backendUser = $this->getBackendUserAuthentication();
+        $resultArray = $this->initializeResultArray();
 
         $table = $this->data['tableName'];
         $fieldName = $this->data['fieldName'];
@@ -56,8 +112,6 @@ class GroupElement extends AbstractFormElement
         $config = $parameterArray['fieldConf']['config'];
         $elementName = $parameterArray['itemFormElName'];
 
-        $resultArray = $this->initializeResultArray();
-
         $selectedItems = $parameterArray['itemFormElValue'];
         $selectedItemsCount = count($selectedItems);
 
@@ -71,73 +125,12 @@ class GroupElement extends AbstractFormElement
             $size = MathUtility::forceIntegerInRange($selectedItemsCount + 1, MathUtility::forceIntegerInRange($size, 1), $autoSizeMax);
         }
 
-        $isDisabled = false;
-        if (isset($config['readOnly']) && $config['readOnly']) {
-            $isDisabled = true;
-        }
-        $showMoveIcons = true;
-        if (isset($config['hideMoveIcons']) && $config['hideMoveIcons']) {
-            $showMoveIcons = false;
-        }
-
         $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'];
-
-        $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) {
-            // If maxitems==1 then automatically replace the current item (in list and file selector)
-            $resultArray['additionalJavaScriptPost'][] =
-                'TBE_EDITOR.clearBeforeSettingFormValueFromBrowseWin[' . GeneralUtility::quoteJSvalue($elementName) . '] = {'
-                    . 'itemFormElID_file: ' . GeneralUtility::quoteJSvalue($uploadFieldId)
-                . '}';
-            $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] =
-                'setFormValueManipulate(' . GeneralUtility::quoteJSvalue($elementName) . ', \'Remove\');'
-                . $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'];
-        } elseif (!$showListControl) {
-            // If the list controls have been removed and the maximum number is reached, remove the first entry to avoid "write once" field
-            $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] =
-                'setFormValueManipulate(' . GeneralUtility::quoteJSvalue($elementName) . ', \'RemoveFirstIfFull\', ' . $maxItems . ');'
-                . $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'];
-        }
 
         $listOfSelectedValues = [];
-        $thumbnailsHtml = [];
-        $recordsOverviewHtml = [];
         $selectorOptionsHtml = [];
-        $clipboardOnClick = [];
         if ($internalType === 'file_reference' || $internalType === 'file') {
-            $fileFactory = ResourceFactory::getInstance();
             foreach ($selectedItems as $selectedItem) {
                 $uidOrPath = $selectedItem['uidOrPath'];
                 $listOfSelectedValues[] = $uidOrPath;
@@ -147,64 +140,6 @@ class GroupElement extends AbstractFormElement
                     '<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>';
-                            }
-                        }
-                    } 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);
-                        }
-                    }
-                }
-            }
-            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) {
@@ -215,8 +150,7 @@ class GroupElement extends AbstractFormElement
                         . htmlspecialchars($folder)
                     . '</option>';
             }
-        } else {
-            // 'db'
+        } elseif ($internalType === 'db') {
             foreach ($selectedItems as $selectedItem) {
                 $tableWithUid = $selectedItem['table'] . '_' . $selectedItem['uid'];
                 $listOfSelectedValues[] = $tableWithUid;
@@ -229,54 +163,98 @@ class GroupElement extends AbstractFormElement
                     '<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
-                    . ');';
+        } else {
+            throw new \RuntimeException(
+                'internal_type missing on type="group" field',
+                1485007097
+            );
+        }
+
+        if (isset($config['readOnly']) && $config['readOnly']) {
+            // Return early if element is read only
+            $html = [];
+            $html[] = '<div class="t3js-formengine-field-item">';
+            $html[] =   '<div class="form-wizards-wrap">';
+            $html[] =       '<div class="form-wizards-element">';
+            $html[] =           '<select';
+            $html[] =               ' size="' . $size . '"';
+            $html[] =               ' disabled="disabled"';
+            $html[] =               ' class="form-control tceforms-multiselect"';
+            $html[] =               ($maxItems !== 1 && $size !== 1) ? ' multiple="multiple"' : '';
+            $html[] =           '>';
+            $html[] =               implode(LF, $selectorOptionsHtml);
+            $html[] =           '</select>';
+            $html[] =       '</div>';
+            $html[] =       '<div class="form-wizards-items-aside">';
+            $html[] =       '</div>';
+            $html[] =   '</div>';
+            $html[] = '</div>';
+            $resultArray['html'] = implode(LF, $html);
+            return $resultArray;
+        }
+
+        // Need some information if in flex form scope for the suggest element
+        $dataStructureIdentifier = '';
+        $flexFormSheetName = '';
+        $flexFormFieldName = '';
+        $flexFormContainerName = '';
+        $flexFormContainerFieldName = '';
+        if ($this->data['processedTca']['columns'][$fieldName]['config']['type'] === 'flex') {
+            $flexFormConfig = $this->data['processedTca']['columns'][$fieldName];
+            $dataStructureIdentifier = $flexFormConfig['config']['dataStructureIdentifier'];
+            if (!isset($flexFormConfig['config']['dataStructureIdentifier'])) {
+                throw new \RuntimeException(
+                    'A data structure identifier must be set in [\'config\'] part of a flex form.'
+                    . ' This is usually added by TcaFlexPrepare data processor',
+                    1485206970
+                );
+            }
+            if (isset($data['flexFormSheetName'])) {
+                $flexFormSheetName = $data['flexFormSheetName'];
+            }
+            if (isset($data['flexFormFieldName'])) {
+                $flexFormFieldName = $data['flexFormFieldName'];
             }
+            if (isset($data['flexFormContainerName'])) {
+                $flexFormContainerName = $data['flexFormContainerName'];
+            }
+            if (isset($data['flexFormContainerFieldName'])) {
+                $flexFormContainerFieldName = $data['flexFormContainerFieldName'];
+            }
+        }
+        // Get minimum characters for suggest from TCA and override by TsConfig
+        $suggestMinimumCharacters = 0;
+        if (isset($config['suggestOptions']['default']['minimumCharacters'])) {
+            $suggestMinimumCharacters = (int)$config['suggestOptions']['default']['minimumCharacters'];
+        }
+        if (isset($parameterArray['fieldTSConfig']['suggest.']['default.']['minimumCharacters'])) {
+            $suggestMinimumCharacters = (int)$parameterArray['fieldTSConfig']['suggest.']['default.']['minimumCharacters'];
+        }
+        $suggestMinimumCharacters = $suggestMinimumCharacters > 0 ? $suggestMinimumCharacters : 2;
+
+        $itemCanBeSelectedMoreThanOnce = !empty($config['multiple']);
+
+        $showMoveIcons = true;
+        if (isset($config['hideMoveIcons']) && $config['hideMoveIcons']) {
+            $showMoveIcons = false;
+        }
+        $showDeleteControl = true;
+        if (isset($config['hideDeleteIcon']) && $config['hideDeleteIcon']) {
+            $showDeleteControl = false;
+        }
+
+        if ($maxItems === 1) {
+            // If maxItems==1 then automatically replace the current item in list
+            $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] =
+                'setFormValueManipulate(' . GeneralUtility::quoteJSvalue($elementName) . ', \'Remove\');'
+                . $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'];
         }
 
         // 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']
@@ -284,241 +262,140 @@ class GroupElement extends AbstractFormElement
             && $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'];
+
+        $selectorAttributes = [
+            'id' => StringUtility::getUniqueId('tceforms-multiselect-'),
+            'data-formengine-input-name' => htmlspecialchars($elementName),
+            'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
+            'size' => $size,
+        ];
+        $selectorClasses = [
+            'form-control',
+            'tceforms-multiselect',
+        ];
+        if ($maxItems === 1) {
+            $selectorClasses[] = 'form-select-no-siblings';
         }
-        $elementBrowserAllowed = implode(',', $allowed);
-        if (is_array($config['appearance']) && isset($config['appearance']['elementBrowserAllowed'])) {
-            $elementBrowserAllowed = $config['appearance']['elementBrowserAllowed'];
+        $selectorAttributes['class'] = implode(' ', $selectorClasses);
+        if ($maxItems !== 1 && $size !== 1) {
+            $selectorAttributes['multiple'] = 'multiple';
         }
-        $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> ';
-                }
-            }
-        }
+        $legacyWizards = $this->renderWizards();
+        $legacyFieldControlHtml = implode(LF, $legacyWizards['fieldControl']);
+        $legacyFieldWizardHtml = implode(LF, $legacyWizards['fieldWizard']);
 
-        $allowedHtml = [];
-        foreach ($allowed as $item) {
-            $allowedHtml[] = '<span class="label label-success">' . htmlspecialchars(strtoupper($item)) . '</span> ';
-        }
+        $fieldInformationResult = $this->renderFieldInformation();
+        $fieldInformationHtml = $fieldInformationResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
 
-        $disallowedHtml = [];
-        foreach ($disallowed as $item) {
-            $disallowedHtml[] = '<span class="label label-danger">' . htmlspecialchars(strtoupper($item)) . '</span> ';
-        }
+        $fieldControlResult = $this->renderFieldControl();
+        $fieldControlHtml = $legacyFieldControlHtml . $fieldControlResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
 
-        $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) . '"';
+        $fieldWizardResult = $this->renderFieldWizard();
+        $fieldWizardHtml = $legacyFieldWizardHtml . $fieldWizardResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
 
         $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 class="t3js-formengine-field-item">';
+        $html[] =   $fieldInformationHtml;
+        $html[] =   '<div class="form-wizards-wrap">';
+        if ($internalType === 'db' && (!isset($config['hideSuggest']) || (bool)$config['hideSuggest'] !== true)) {
+            $html[] =   '<div class="form-wizards-items-top">';
+            $html[] =       '<div class="autocomplete t3-form-suggest-container">';
+            $html[] =           '<div class="input-group">';
+            $html[] =               '<span class="input-group-addon">';
+            $html[] =                   $this->iconFactory->getIcon('actions-search', Icon::SIZE_SMALL)->render();
+            $html[] =               '</span>';
+            $html[] =               '<input type="search" class="t3-form-suggest form-control"';
+            $html[] =                   ' placeholder="' . $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.findRecord') . '"';
+            $html[] =                   ' data-fieldname="' . htmlspecialchars($fieldName) . '"';
+            $html[] =                   ' data-tablename="' . htmlspecialchars($table) . '"';
+            $html[] =                   ' data-field="' . htmlspecialchars($elementName) . '"';
+            $html[] =                   ' data-uid="' . htmlspecialchars($this->data['databaseRow']['uid']) . '"';
+            $html[] =                   ' data-pid="' . htmlspecialchars($this->data['effectivePid']) . '"';
+            $html[] =                   ' data-fieldtype="' . htmlspecialchars($config['type']) . '"';
+            $html[] =                   ' data-minchars="' . htmlspecialchars($suggestMinimumCharacters) . '"';
+            $html[] =                   ' data-datastructureidentifier="' . htmlspecialchars($dataStructureIdentifier) . '"';
+            $html[] =                   ' data-flexformsheetname="' . htmlspecialchars($flexFormSheetName) . '"';
+            $html[] =                   ' data-flexformfieldname="' . htmlspecialchars($flexFormFieldName) . '"';
+            $html[] =                   ' data-flexformcontainername="' . htmlspecialchars($flexFormContainerName) . '"';
+            $html[] =                   ' data-flexformcontainerfieldname="' . htmlspecialchars($flexFormContainerFieldName) . '"';
+            $html[] =               '/>';
+            $html[] =           '</div>';
             $html[] =       '</div>';
+            $html[] =   '</div>';
         }
-        $html[] =   '</div>';
-
-        $html[] =   '<div class="form-wizards-items">';
-        $html[] =       '<div class="btn-group-vertical">';
-        if ($maxItems > 1 && $size >=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 && !$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 class="form-wizards-element">';
+        $html[] =           '<input type="hidden" class="t3js-group-hidden-field" data-formengine-input-name="' . htmlspecialchars($elementName) . '" value="' . $itemCanBeSelectedMoreThanOnce . '" />';
+        $html[] =           '<select ' . GeneralUtility::implodeAttributes($selectorAttributes, true) . '>';
+        $html[] =               implode(LF, $selectorOptionsHtml);
+        $html[] =           '</select>';
         $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 class="form-wizards-items-aside">';
+        $html[] =           '<div class="btn-group-vertical">';
+        if ($maxItems > 1 && $size >=5 && $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 && $size > 1 && $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 && $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) {
+            $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-aside">';
+        $html[] =           '<div class="btn-group-vertical">';
+        $html[] =               $fieldControlHtml;
+        $html[] =           '</div>';
+        $html[] =       '</div>';
+        $html[] =       '<div class="form-wizards-items-bottom">';
+        $html[] =           $fieldWizardHtml;
         $html[] =       '</div>';
         $html[] =   '</div>';
+        $html[] =   '<input type="hidden" name="' . htmlspecialchars($elementName) . '" value="' . htmlspecialchars(implode(',', $listOfSelectedValues)) . '" />';
         $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 = '[]';
-                }
-                $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 ],
-                $config['wizards'],
-                $table,
-                $row,
-                $fieldName,
-                $parameterArray,
-                $elementName,
-                BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras'])
-            );
-        }
-
-        $resultArray['html'] = $html;
+        $resultArray['html'] = implode(LF, $html);
         return $resultArray;
     }
 
@@ -529,4 +406,12 @@ class GroupElement extends AbstractFormElement
     {
         return $GLOBALS['BE_USER'];
     }
+
+    /**
+     * @return LanguageService
+     */
+    protected function getLanguageService()
+    {
+        return $GLOBALS['LANG'];
+    }
 }
index f69794a..ea07e6e 100644 (file)
@@ -29,6 +29,23 @@ use TYPO3\CMS\Core\Utility\StringUtility;
 class ImageManipulationElement extends AbstractFormElement
 {
     /**
+     * Default field wizards enabled for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldWizard = [
+        'otherLanguageContent' => [
+            'renderType' => 'otherLanguageContent',
+        ],
+        'defaultLanguageDifferences' => [
+            'renderType' => 'defaultLanguageDifferences',
+            'after' => [
+                'otherLanguageContent',
+            ],
+        ],
+    ];
+
+    /**
      * Default element configuration
      *
      * @var array
@@ -70,15 +87,16 @@ class ImageManipulationElement extends AbstractFormElement
         }
 
         if ($config['readOnly']) {
-            $options = [];
-            $options['parameterArray'] = [
-                'fieldConf' => [
-                    'config' => $config,
-                ],
-                'itemFormElValue' => $parameterArray['itemFormElValue'],
-            ];
-            $options['renderType'] = 'none';
-            return $this->nodeFactory->create($options)->render();
+            $html = [];
+            $html[] = '<div class="t3js-formengine-field-item">';
+            $html[] =   '<div class="form-wizards-wrap">';
+            $html[] =       '<div class="form-wizards-element">';
+            $html[] =           htmlspecialchars($parameterArray['itemFormElValue']);
+            $html[] =       '</div>';
+            $html[] =   '</div>';
+            $html[] = '</div>';
+            $resultArray['html'] = implode(LF, $html);
+            return $resultArray;
         }
 
         $file = $this->getFile($row, $config['file_field']);
@@ -154,6 +172,36 @@ class ImageManipulationElement extends AbstractFormElement
         $item .= '<div class="media-body">' . $content . '</div>';
         $item .= '</div>';
 
+        $fieldInformationResult = $this->renderFieldInformation();
+        $fieldInformationHtml = $fieldInformationResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
+
+        $fieldControlResult = $this->renderFieldControl();
+        $fieldControlHtml = $fieldControlResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
+
+        $fieldWizardResult = $this->renderFieldWizard();
+        $fieldWizardHtml = $fieldWizardResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
+
+        $html = [];
+        $html[] = '<div class="t3js-formengine-field-item">';
+        $html[] =   $fieldInformationHtml;
+        $html[] =   '<div class="form-wizards-wrap">';
+        $html[] =       '<div class="form-wizards-element">';
+        $html[] =           $item;
+        $html[] =       '</div>';
+        $html[] =      '<div class="form-wizards-items-aside">';
+        $html[] =          '<div class="btn-group">';
+        $html[] =              $fieldControlHtml;
+        $html[] =          '</div>';
+        $html[] =      '</div>';
+        $html[] =       '<div class="form-wizards-items-bottom">';
+        $html[] =           $fieldWizardHtml;
+        $html[] =       '</div>';
+        $html[] =   '</div>';
+        $html[] = '</div>';
+
         $resultArray['html'] = $item;
         return $resultArray;
     }
index 2bb6895..f7c83a8 100644 (file)
@@ -14,152 +14,228 @@ namespace TYPO3\CMS\Backend\Form\Element;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
+use TYPO3\CMS\Lang\LanguageService;
 
 /**
- * Class InputColorPickerElement
+ * Render an input field with a color picker
  */
 class InputColorPickerElement extends AbstractFormElement
 {
     /**
+     * Default field wizards enabled for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldWizard = [
+        'otherLanguageContent' => [
+            'renderType' => 'otherLanguageContent',
+        ],
+        'defaultLanguageDifferences' => [
+            'renderType' => 'defaultLanguageDifferences',
+            'after' => [
+                'otherLanguageContent',
+            ],
+        ],
+    ];
+
+    /**
      * This will render a single-line input form field, possibly with various control/validation features
      *
      * @return array As defined in initializeResultArray() of AbstractNode
      */
     public function render()
     {
+        $languageService = $this->getLanguageService();
+
         $table = $this->data['tableName'];
         $fieldName = $this->data['fieldName'];
         $row = $this->data['databaseRow'];
         $parameterArray = $this->data['parameterArray'];
         $resultArray = $this->initializeResultArray();
 
+        $itemValue = $parameterArray['itemFormElValue'];
         $config = $parameterArray['fieldConf']['config'];
-        $specConf = BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras']);
         $size = MathUtility::forceIntegerInRange($config['size'] ?: $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth);
         $evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
-        $classes = [];
-        $attributes = [];
+        $width = (int)$this->formMaxWidth($size);
+        $nullControlNameAttribute = ' name="' . htmlspecialchars('control[active][' . $table . '][' . $row['uid'] . '][' . $fieldName . ']') . '"';
 
-        // readonly
         if ($config['readOnly']) {
-            $itemFormElValue = $parameterArray['itemFormElValue'];
-            $options = $this->data;
-            $options['parameterArray'] = [
-                'fieldConf' => [
-                    'config' => $config,
-                ],
-                'itemFormElValue' => $itemFormElValue,
-            ];
-            $options['renderType'] = 'none';
-            return $this->nodeFactory->create($options)->render();
+            $html = [];
+            $html[] = '<div class="t3js-formengine-field-item">';
+            $html[] =   '<div class="form-wizards-wrap">';
+            $html[] =       '<div class="form-wizards-element">';
+            $html[] =           '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
+            $html[] =               '<input class="form-control" value="' . htmlspecialchars($itemValue) . '" type="text" disabled>';
+            $html[] =           '</div>';
+            $html[] =       '</div>';
+            $html[] =   '</div>';
+            $html[] = '</div>';
+            $resultArray['html'] = implode(LF, $html);
+            return $resultArray;
         }
 
         // @todo: The whole eval handling is a mess and needs refactoring
         foreach ($evalList as $func) {
-            switch ($func) {
-                case 'required':
-                    $attributes['data-formengine-validation-rules'] = $this->getValidationDataAsJsonString(['required' => true]);
-                    break;
-                default:
-                    // @todo: This is ugly: The code should find out on it's own whether a eval definition is a
-                    // @todo: keyword like "date", or a class reference. The global registration could be dropped then
-                    // Pair hook to the one in \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval()
-                    if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
-                        if (class_exists($func)) {
-                            $evalObj = GeneralUtility::makeInstance($func);
-                            if (method_exists($evalObj, 'deevaluateFieldValue')) {
-                                $_params = [
-                                    'value' => $parameterArray['itemFormElValue']
-                                ];
-                                $parameterArray['itemFormElValue'] = $evalObj->deevaluateFieldValue($_params);
-                            }
-                        }
+            // @todo: This is ugly: The code should find out on it's own whether a eval definition is a
+            // @todo: keyword like "date", or a class reference. The global registration could be dropped then
+            // Pair hook to the one in \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval()
+            if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
+                if (class_exists($func)) {
+                    $evalObj = GeneralUtility::makeInstance($func);
+                    if (method_exists($evalObj, 'deevaluateFieldValue')) {
+                        $_params = [
+                            'value' => $itemValue
+                        ];
+                        $itemValue = $evalObj->deevaluateFieldValue($_params);
                     }
+                    if (method_exists($evalObj, 'returnFieldJS')) {
+                        $resultArray['additionalJavaScriptPost'][] = 'TBE_EDITOR.customEvalFunctions[' . GeneralUtility::quoteJSvalue($evalData) . '] = function(value) {' . $evalObj->returnFieldJS() . '};';
+                    }
+                }
             }
         }
 
-        $paramsList = [
-            'field' => $parameterArray['itemFormElName'],
-            'evalList' => implode(',', $evalList),
-            'is_in' => trim($config['is_in']),
-        ];
-
-        // set classes
-        $classes[] = 'form-control';
-        $classes[] = 'hasDefaultValue';
-        $classes[] = 't3js-clearable';
-        $classes[] = 't3js-color-picker';
-        $classes[] = 'formengine-colorpickerelement';
-        $attributes['class'] = implode(' ', $classes);
-
         // Load needed js library
         $resultArray['requireJsModules'][] = [
             'TYPO3/CMS/Backend/ColorPicker' => 'function(ColorPicker){ColorPicker.initialize()}'
         ];
 
-        // calculate attributes
-        $attributes['data-formengine-validation-rules'] = $this->getValidationDataAsJsonString($config);
-        $attributes['data-formengine-input-params'] = json_encode($paramsList);
-        $attributes['data-formengine-input-name'] = $parameterArray['itemFormElName'];
-        $attributes['id'] = StringUtility::getUniqueId('formengine-input-');
-        $attributes['value'] = '';
+        $attributes = [
+            'value' => $itemValue,
+            'id' => StringUtility::getUniqueId('formengine-input-'),
+            'class' => implode(' ', [
+                'form-control',
+                'hasDefaultValue',
+                't3js-clearable',
+                't3js-color-picker',
+                'formengine-colorpickerelement',
+            ]),
+            'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
+            'data-formengine-input-params' => json_encode([
+                'field' => $parameterArray['itemFormElName'],
+                'evalList' => implode(',', $evalList),
+                'is_in' => trim($config['is_in']),
+            ]),
+            'data-formengine-input-name' => $parameterArray['itemFormElName'],
+        ];
+
         if (isset($config['max']) && (int)$config['max'] > 0) {
             $attributes['maxlength'] = (int)$config['max'];
         }
-
-        // This is the EDITABLE form field.
         if (!empty($config['placeholder'])) {
             $attributes['placeholder'] = trim($config['placeholder']);
         }
-
         if (isset($config['autocomplete'])) {
             $attributes['autocomplete'] = empty($config['autocomplete']) ? 'new-' . $fieldName : 'on';
         }
 
-        // Build the attribute string
-        $attributeString = '';
-        foreach ($attributes as $attributeName => $attributeValue) {
-            $attributeString .= ' ' . $attributeName . '="' . htmlspecialchars($attributeValue) . '"';
-        }
-
-        $html = '<input type="text" value="' . htmlspecialchars($parameterArray['itemFormElValue']) . '" ' . $attributeString . ' />';
-
-        // This is the ACTUAL form field - values from the EDITABLE field must be transferred to this field which is the one that is written to the database.
-        $html .= '<input type="hidden" name="' . htmlspecialchars($parameterArray['itemFormElName']) . '" value="' . htmlspecialchars($parameterArray['itemFormElValue']) . '" />';
-
-        // Going through all custom evaluations configured for this field
-        // @todo: Similar to above code!
-        foreach ($evalList as $evalData) {
-            if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$evalData])) {
-                if (class_exists($evalData)) {
-                    $evalObj = GeneralUtility::makeInstance($evalData);
-                    if (method_exists($evalObj, 'returnFieldJS')) {
-                        $resultArray['additionalJavaScriptPost'][] = 'TBE_EDITOR.customEvalFunctions[' . GeneralUtility::quoteJSvalue($evalData) . '] = function(value) {' . $evalObj->returnFieldJS() . '};';
-                    }
+        $legacyWizards = $this->renderWizards();
+        $legacyFieldControlHtml = implode(LF, $legacyWizards['fieldControl']);
+        $legacyFieldWizardHtml = implode(LF, $legacyWizards['fieldWizard']);
+
+        $fieldInformationResult = $this->renderFieldInformation();
+        $fieldInformationHtml = $fieldInformationResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
+
+        $fieldWizardResult = $this->renderFieldWizard();
+        $fieldWizardHtml = $legacyFieldWizardHtml . $fieldWizardResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
+
+        $fieldControlResult = $this->renderFieldControl();
+        $fieldControlHtml = $legacyFieldControlHtml . $fieldControlResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
+
+        $mainFieldHtml = [];
+        $mainFieldHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
+        $mainFieldHtml[] =  '<div class="form-wizards-wrap">';
+        $mainFieldHtml[] =      '<div class="form-wizards-element">';
+        $mainFieldHtml[] =          '<input type="text"' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
+        $mainFieldHtml[] =          '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($itemValue) . '" />';
+        $mainFieldHtml[] =      '</div>';
+        $mainFieldHtml[] =      '<div class="form-wizards-items-aside">';
+        $mainFieldHtml[] =          '<div class="btn-group">';
+        $mainFieldHtml[] =              $fieldControlHtml;
+        $mainFieldHtml[] =          '</div>';
+        $mainFieldHtml[] =      '</div>';
+        $mainFieldHtml[] =      '<div class="form-wizards-items-bottom">';
+        $mainFieldHtml[] =          $fieldWizardHtml;
+        $mainFieldHtml[] =      '</div>';
+        $mainFieldHtml[] =  '</div>';
+        $mainFieldHtml[] = '</div>';
+        $mainFieldHtml = implode(LF, $mainFieldHtml);
+
+        $fullElement = $mainFieldHtml;
+        if ($this->hasNullCheckboxButNoPlaceholder()) {
+            $checked = $itemValue !== null ? ' checked="checked"' : '';
+            $fullElement = [];
+            $fullElement[] = '<div class="t3-form-field-disable"></div>';
+            $fullElement[] = '<div class="checkbox t3-form-field-eval-null-checkbox">';
+            $fullElement[] =     '<label>';
+            $fullElement[] =         '<input type="hidden"' . $nullControlNameAttribute . ' value="0" />';
+            $fullElement[] =         '<input type="checkbox"' . $nullControlNameAttribute . ' value="1"' . $checked . ' />';
+            $fullElement[] =         $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
+            $fullElement[] =     '</label>';
+            $fullElement[] = '</div>';
+            $fullElement[] = $mainFieldHtml;
+            $fullElement = implode(LF, $fullElement);
+        } elseif ($this->hasNullCheckboxWithPlaceholder()) {
+            $checked = $itemValue !== null ? ' checked="checked"' : '';
+            $placeholder = $shortenedPlaceholder = $config['placeholder'] ?? '';
+            $disabled = '';
+            $fallbackValue = 0;
+            if (strlen($placeholder) > 0) {
+                $shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
+                if ($placeholder !== $shortenedPlaceholder) {
+                    $overrideLabel = sprintf(
+                        $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
+                        '<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
+                    );
+                } else {
+                    $overrideLabel = sprintf(
+                        $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
+                        htmlspecialchars($placeholder)
+                    );
                 }
+            } else {
+                $fallbackValue = 1;
+                $checked = ' checked="checked"';
+                $disabled = ' disabled="disabled"';
+                $overrideLabel = $languageService->sL(
+                    'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
+                );
             }
+            $fullElement = [];
+            $fullElement[] = '<div class="checkbox t3js-form-field-eval-null-placeholder-checkbox">';
+            $fullElement[] =     '<label>';
+            $fullElement[] =         '<input type="hidden"' . $nullControlNameAttribute . ' value="' . $fallbackValue . '" />';
+            $fullElement[] =         '<input type="checkbox"' . $nullControlNameAttribute . ' value="1"' . $checked . $disabled . ' />';
+            $fullElement[] =         $overrideLabel;
+            $fullElement[] =     '</label>';
+            $fullElement[] = '</div>';
+            $fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
+            $fullElement[] =    '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
+            $fullElement[] =        '<input type="text" class="form-control" disabled="disabled" value="' . $shortenedPlaceholder . '" />';
+            $fullElement[] =    '</div>';
+            $fullElement[] = '</div>';
+            $fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
+            $fullElement[] =    $mainFieldHtml;
+            $fullElement[] = '</div>';
+            $fullElement = implode(LF, $fullElement);
         }
 
-        // Wrap a wizard around the item?
-        $html = $this->renderWizards(
-            [$html],
-            $config['wizards'],
-            $table,
-            $row,
-            $fieldName,
-            $parameterArray,
-            $parameterArray['itemFormElName'],
-            $specConf
-        );
-
-        // Add a wrapper to remain maximum width
-        $width = (int)$this->formMaxWidth($size);
-        $html = '<div class="form-control-wrap"' . ($width ? ' style="max-width: ' . $width . 'px"' : '') . '>' . $html . '</div>';
-        $resultArray['html'] = $html;
+        $resultArray['html'] = '<div class="t3js-formengine-field-item">' . $fieldInformationHtml . $fullElement . '</div>';
         return $resultArray;
     }
+
+    /**
+     * @return LanguageService
+     */
+    protected function getLanguageService()
+    {
+        return $GLOBALS['LANG'];
+    }
 }
diff --git a/typo3/sysext/backend/Classes/Form/Element/InputDateTimeElement.php b/typo3/sysext/backend/Classes/Form/Element/InputDateTimeElement.php
new file mode 100644 (file)
index 0000000..2002f1b
--- /dev/null
@@ -0,0 +1,261 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Element;
+
+/*
+ * 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 TYPO3\CMS\Core\Imaging\Icon;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MathUtility;
+use TYPO3\CMS\Core\Utility\StringUtility;
+use TYPO3\CMS\Lang\LanguageService;
+
+/**
+ * Generation of TCEform elements of the type "input type=text"
+ */
+class InputDateTimeElement extends AbstractFormElement
+{
+    /**
+     * Default field wizards enabled for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldWizard = [
+        'otherLanguageContent' => [
+            'renderType' => 'otherLanguageContent',
+        ],
+        'defaultLanguageDifferences' => [
+            'renderType' => 'defaultLanguageDifferences',
+            'after' => [
+                'otherLanguageContent',
+            ],
+        ],
+    ];
+
+    /**
+     * This will render a single-line input form field, possibly with various control/validation features
+     *
+     * @return array As defined in initializeResultArray() of AbstractNode
+     * @throws \RuntimeException with invalid configuration
+     */
+    public function render()
+    {
+        $languageService = $this->getLanguageService();
+
+        $table = $this->data['tableName'];
+        $fieldName = $this->data['fieldName'];
+        $row = $this->data['databaseRow'];
+        $parameterArray = $this->data['parameterArray'];
+        $resultArray = $this->initializeResultArray();
+        $config = $parameterArray['fieldConf']['config'];
+
+        $itemValue = $parameterArray['itemFormElValue'];
+        $defaultInputWidth = 10;
+        $evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
+        $nullControlNameAttribute = ' name="' . htmlspecialchars('control[active][' . $table . '][' . $row['uid'] . '][' . $fieldName . ']') . '"';
+
+        if (in_array('date', $evalList, true)) {
+            $format = 'date';
+            $defaultInputWidth = 13;
+        } elseif (in_array('datetime', $evalList, true)) {
+            $format = 'datetime';
+            $defaultInputWidth = 13;
+        } elseif (in_array('time', $evalList, true)) {
+            $format = 'time';
+        } elseif (in_array('timesec', $evalList, true)) {
+            $format = 'timesec';
+        } else {
+            throw new \RuntimeException(
+                'Field "' . $fieldName . '" in table "' . $table . '" with renderType "inputDataTime" needs'
+                . '"eval" set to either "date", "datetime", "time" or "timesec"',
+                1483823746
+            );
+        }
+
+        $size = MathUtility::forceIntegerInRange($config['size'] ?? $defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth);
+        $width = (int)$this->formMaxWidth($size);
+
+        if (isset($config['readOnly']) && $config['readOnly']) {
+            // Early return for read only fields
+            $itemValue = $this->formatValue($format, $itemValue);
+            $html = [];
+            $html[] = '<div class="t3js-formengine-field-item">';
+            $html[] =   '<div class="form-wizards-wrap">';
+            $html[] =       '<div class="form-wizards-element">';
+            $html[] =           '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
+            $html[] =               '<input class="form-control" value="' . htmlspecialchars($itemValue) . '" type="text" disabled>';
+            $html[] =           '</div>';
+            $html[] =       '</div>';
+            $html[] =   '</div>';
+            $html[] = '</div>';
+            $resultArray['html'] = implode(LF, $html);
+            return $resultArray;
+        }
+
+        $attributes = [
+            'value' => '',
+            'id' => StringUtility::getUniqueId('formengine-input-'),
+            'class' => implode(' ', [
+                't3js-datetimepicker',
+                'form-control',
+                't3js-clearable',
+                'hasDefaultValue',
+            ]),
+            'data-date-type' => $format,
+            'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
+            'data-formengine-input-params' => json_encode([
+                'field' => $parameterArray['itemFormElName'],
+                'evalList' => implode(',', $evalList)
+            ]),
+            'data-formengine-input-name' => $parameterArray['itemFormElName'],
+        ];
+
+        $maxLength = $config['max'] ?? 0;
+        if ((int)$maxLength > 0) {
+            $attributes['maxlength'] = (int)$maxLength;
+        }
+        if (!empty($config['placeholder'])) {
+            $attributes['placeholder'] = trim($config['placeholder']);
+        }
+
+        if ($format === 'datetime' || $format === 'date') {
+            // convert timestamp to proper ISO-8601 date so we get rid of timezone issues on the client.
+            // This only handles integer timestamps; if the field is a date(time), it already was converted to an
+            // ISO-8601 date by DatabaseRowDateTimeFields.
+            if (MathUtility::canBeInterpretedAsInteger($itemValue) && $itemValue != 0) {
+                // output date as a ISO-8601 date; the stored value is the server time zone, so we need to treat it as such.
+                $timestamp = $itemValue;
+                $timestamp += date('Z', $timestamp);
+                $itemValue = gmdate('c', $timestamp);
+            }
+            if (isset($config['range']['lower'])) {
+                $attributes['data-date-minDate'] = (int)$config['range']['lower'];
+            }
+            if (isset($config['range']['upper'])) {
+                $attributes['data-date-maxDate'] = (int)$config['range']['upper'];
+            }
+        }
+
+        $legacyWizards = $this->renderWizards();
+        $legacyFieldControlHtml = implode(LF, $legacyWizards['fieldControl']);
+        $legacyFieldWizardHtml = implode(LF, $legacyWizards['fieldWizard']);
+
+        $fieldInformationResult = $this->renderFieldInformation();
+        $fieldInformationHtml = $fieldInformationResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
+
+        $fieldWizardResult = $this->renderFieldWizard();
+        $fieldWizardHtml = $legacyFieldWizardHtml . $fieldWizardResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
+
+        $fieldControlResult = $this->renderFieldControl();
+        $fieldControlHtml = $legacyFieldControlHtml . $fieldControlResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
+
+        $expansionHtml = [];
+        $expansionHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
+        $expansionHtml[] =  '<div class="form-wizards-wrap">';
+        $expansionHtml[] =      '<div class="form-wizards-element">';
+        $expansionHtml[] =          '<div class="input-group">';
+        $expansionHtml[] =              '<input type="text"' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
+        $expansionHtml[] =              '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($itemValue) . '" />';
+        $expansionHtml[] =              '<span class="input-group-btn">';
+        $expansionHtml[] =                  '<label class="btn btn-default" for="' . $attributes['id'] . '">';
+        $expansionHtml[] =                      $this->iconFactory->getIcon('actions-edit-pick-date', Icon::SIZE_SMALL)->render();
+        $expansionHtml[] =                  '</label>';
+        $expansionHtml[] =              '</span>';
+        $expansionHtml[] =          '</div>';
+        $expansionHtml[] =      '</div>';
+        $expansionHtml[] =      '<div class="form-wizards-items-aside">';
+        $expansionHtml[] =          '<div class="btn-group">';
+        $expansionHtml[] =              $fieldControlHtml;
+        $expansionHtml[] =          '</div>';
+        $expansionHtml[] =      '</div>';
+        $expansionHtml[] =      '<div class="form-wizards-items-bottom">';
+        $expansionHtml[] =          $fieldWizardHtml;
+        $expansionHtml[] =      '</div>';
+        $expansionHtml[] =  '</div>';
+        $expansionHtml[] = '</div>';
+        $expansionHtml = implode(LF, $expansionHtml);
+
+        $fullElement = $expansionHtml;
+        if ($this->hasNullCheckboxButNoPlaceholder()) {
+            $checked = $itemValue !== null ? ' checked="checked"' : '';
+            $fullElement = [];
+            $fullElement[] = '<div class="t3-form-field-disable"></div>';
+            $fullElement[] = '<div class="checkbox t3-form-field-eval-null-checkbox">';
+            $fullElement[] =     '<label>';
+            $fullElement[] =         '<input type="hidden"' . $nullControlNameAttribute . ' value="0" />';
+            $fullElement[] =         '<input type="checkbox"' . $nullControlNameAttribute . ' value="1"' . $checked . ' />';
+            $fullElement[] =         $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
+            $fullElement[] =     '</label>';
+            $fullElement[] = '</div>';
+            $fullElement[] = $expansionHtml;
+            $fullElement = implode(LF, $fullElement);
+        } elseif ($this->hasNullCheckboxWithPlaceholder()) {
+            $checked = $itemValue !== null ? ' checked="checked"' : '';
+            $placeholder = $shortenedPlaceholder = $config['placeholder'] ?? '';
+            $disabled = '';
+            $fallbackValue = 0;
+            if (strlen($placeholder) > 0) {
+                $shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
+                if ($placeholder !== $shortenedPlaceholder) {
+                    $overrideLabel = sprintf(
+                        $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
+                        '<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
+                    );
+                } else {
+                    $overrideLabel = sprintf(
+                        $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
+                        htmlspecialchars($placeholder)
+                    );
+                }
+            } else {
+                $fallbackValue = 1;
+                $checked = ' checked="checked"';
+                $disabled = ' disabled="disabled"';
+                $overrideLabel = $languageService->sL(
+                    'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
+                );
+            }
+            $fullElement = [];
+            $fullElement[] = '<div class="checkbox t3js-form-field-eval-null-placeholder-checkbox">';
+            $fullElement[] =     '<label>';
+            $fullElement[] =         '<input type="hidden"' . $nullControlNameAttribute . ' value="' . $fallbackValue . '" />';
+            $fullElement[] =         '<input type="checkbox"' . $nullControlNameAttribute . ' value="1"' . $checked . $disabled . ' />';
+            $fullElement[] =         $overrideLabel;
+            $fullElement[] =     '</label>';
+            $fullElement[] = '</div>';
+            $fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
+            $fullElement[] =    '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
+            $fullElement[] =        '<input type="text" class="form-control" disabled="disabled" value="' . $shortenedPlaceholder . '" />';
+            $fullElement[] =    '</div>';
+            $fullElement[] = '</div>';
+            $fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
+            $fullElement[] =    $expansionHtml;
+            $fullElement[] = '</div>';
+            $fullElement = implode(LF, $fullElement);
+        }
+
+        $resultArray['html'] = '<div class="t3js-formengine-field-item">' . $fieldInformationHtml . $fullElement . '</div>';
+        return $resultArray;
+    }
+
+    /**
+     * @return LanguageService
+     */
+    protected function getLanguageService(): LanguageService
+    {
+        return $GLOBALS['LANG'];
+    }
+}
diff --git a/typo3/sysext/backend/Classes/Form/Element/InputLinkElement.php b/typo3/sysext/backend/Classes/Form/Element/InputLinkElement.php
new file mode 100644 (file)
index 0000000..796d8d3
--- /dev/null
@@ -0,0 +1,279 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Element;
+
+/*
+ * 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 TYPO3\CMS\Backend\Form\FieldWizard\DefaultLanguageDifferences;
+use TYPO3\CMS\Backend\Form\FieldWizard\OtherLanguageContent;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MathUtility;
+use TYPO3\CMS\Core\Utility\StringUtility;
+use TYPO3\CMS\Lang\LanguageService;
+
+/**
+ * Link input element.
+ *
+ * Shows current link and the link popup.
+ */
+class InputLinkElement extends AbstractFormElement
+{
+    /**
+     * Default field controls render the link icon
+     *
+     * @var array
+     */
+    protected $defaultFieldControl = [
+        'linkPopup' => [
+            'renderType' => 'linkPopup',
+            'options' => []
+        ],
+    ];
+
+    /**
+     * Default field wizards enabled for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldWizard = [
+        OtherLanguageContent::class => [
+            'renderType' => 'otherLanguageContent',
+        ],
+        DefaultLanguageDifferences::class => [
+            'renderType' => 'defaultLanguageDifferences',
+            'after' => [
+                OtherLanguageContent::class
+            ],
+        ],
+    ];
+
+    /**
+     * This will render a single-line input form field, possibly with various control/validation features
+     *
+     * @return array As defined in initializeResultArray() of AbstractNode
+     */
+    public function render()
+    {
+        $languageService = $this->getLanguageService();
+
+        $table = $this->data['tableName'];
+        $fieldName = $this->data['fieldName'];
+        $row = $this->data['databaseRow'];
+        $parameterArray = $this->data['parameterArray'];
+        $resultArray = $this->initializeResultArray();
+        $config = $parameterArray['fieldConf']['config'];
+
+        $itemValue = $parameterArray['itemFormElValue'];
+        $evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
+        $size = MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth);
+        $width = (int)$this->formMaxWidth($size);
+        $nullControlNameAttribute = ' name="' . htmlspecialchars('control[active][' . $table . '][' . $row['uid'] . '][' . $fieldName . ']') . '"';
+
+        if ($config['readOnly']) {
+            // Early return for read only fields
+            $html = [];
+            $html[] = '<div class="t3js-formengine-field-item">';
+            $html[] =   '<div class="form-wizards-wrap">';
+            $html[] =       '<div class="form-wizards-element">';
+            $html[] =           '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
+            $html[] =               '<input class="form-control" value="' . htmlspecialchars($itemValue) . '" type="text" disabled>';
+            $html[] =           '</div>';
+            $html[] =       '</div>';
+            $html[] =   '</div>';
+            $html[] = '</div>';
+            $resultArray['html'] = implode(LF, $html);
+            return $resultArray;
+        }
+
+        // @todo: The whole eval handling is a mess and needs refactoring
+        foreach ($evalList as $func) {
+            // @todo: This is ugly: The code should find out on it's own whether a eval definition is a
+            // @todo: keyword like "date", or a class reference. The global registration could be dropped then
+            // Pair hook to the one in \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval()
+            if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
+                if (class_exists($func)) {
+                    $evalObj = GeneralUtility::makeInstance($func);
+                    if (method_exists($evalObj, 'deevaluateFieldValue')) {
+                        $_params = [
+                            'value' => $itemValue
+                        ];
+                        $itemValue = $evalObj->deevaluateFieldValue($_params);
+                    }
+                    if (method_exists($evalObj, 'returnFieldJS')) {
+                        $resultArray['additionalJavaScriptPost'][] = 'TBE_EDITOR.customEvalFunctions[' . GeneralUtility::quoteJSvalue($func) . ']'
+                            . ' = function(value) {' . $evalObj->returnFieldJS() . '};';
+                    }
+                }
+            }
+        }
+
+        $attributes = [
+            'value' => '',
+            'id' => StringUtility::getUniqueId('formengine-input-'),
+            'class' => implode(' ', [
+                'form-control',
+                't3js-clearable',
+                'hasDefaultValue',
+            ]),
+            'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
+            'data-formengine-input-params' => json_encode([
+                'field' => $parameterArray['itemFormElName'],
+                'evalList' => implode(',', $evalList)
+            ]),
+            'data-formengine-input-name' => $parameterArray['itemFormElName'],
+        ];
+
+        $maxLength = $config['max'] ?? 0;
+        if ((int)$maxLength > 0) {
+            $attributes['maxlength'] = (int)$maxLength;
+        }
+        if (!empty($config['placeholder'])) {
+            $attributes['placeholder'] = trim($config['placeholder']);
+        }
+        if (isset($config['autocomplete'])) {
+            $attributes['autocomplete'] = empty($config['autocomplete']) ? 'new-' . $fieldName : 'on';
+        }
+
+        $valuePickerHtml = [];
+        if (isset($config['valuePicker']['items']) && is_array($config['valuePicker']['items'])) {
+            $mode = $config['valuePicker']['mode'] ?? '';
+            $itemName = $parameterArray['itemFormElName'];
+            $fieldChangeFunc = $parameterArray['fieldChangeFunc'];
+            if ($mode === 'append') {
+                $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
+                    . '.value=\'\'+this.options[this.selectedIndex].value+document.editform[' . GeneralUtility::quoteJSvalue($itemName) . '].value';
+            } elseif ($mode === 'prepend') {
+                $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
+                    . '.value+=\'\'+this.options[this.selectedIndex].value';
+            } else {
+                $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
+                    . '.value=this.options[this.selectedIndex].value';
+            }
+            $valuePickerHtml[] = '<select';
+            $valuePickerHtml[] =  ' class="form-control tceforms-select tceforms-wizardselect"';
+            $valuePickerHtml[] =  ' onchange="' . htmlspecialchars($assignValue . ';this.blur();this.selectedIndex=0;' . implode('', $fieldChangeFunc)) . '"';
+            $valuePickerHtml[] = '>';
+            $valuePickerHtml[] = '<option></option>';
+            foreach ($config['valuePicker']['items'] as $item) {
+                $valuePickerHtml[] = '<option value="' . htmlspecialchars($item[1]) . '">' . htmlspecialchars($languageService->sL($item[0])) . '</option>';
+            }
+            $valuePickerHtml[] = '</select>';
+        }
+
+        $legacyWizards = $this->renderWizards();
+        $legacyFieldControlHtml = implode(LF, $legacyWizards['fieldControl']);
+        $legacyFieldWizardHtml = implode(LF, $legacyWizards['fieldWizard']);
+
+        $fieldInformationResult = $this->renderFieldInformation();
+        $fieldInformationHtml = $fieldInformationResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
+
+        $fieldWizardResult = $this->renderFieldWizard();
+        $fieldWizardHtml = $legacyFieldWizardHtml . $fieldWizardResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
+
+        $fieldControlResult = $this->renderFieldControl();
+        $fieldControlHtml = $legacyFieldControlHtml . $fieldControlResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
+
+        $expansionHtml = [];
+        $expansionHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
+        $expansionHtml[] =  '<div class="form-wizards-wrap">';
+        $expansionHtml[] =      '<div class="form-wizards-element">';
+        $expansionHtml[] =          '<input type="text"' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
+        $expansionHtml[] =          '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($itemValue) . '" />';
+        $expansionHtml[] =      '</div>';
+        $expansionHtml[] =      '<div class="form-wizards-items-aside">';
+        $expansionHtml[] =          '<div class="btn-group">';
+        $expansionHtml[] =              implode(LF, $valuePickerHtml);
+        $expansionHtml[] =              $fieldControlHtml;
+        $expansionHtml[] =          '</div>';
+        $expansionHtml[] =      '</div>';
+        $expansionHtml[] =      '<div class="form-wizards-items-bottom">';
+        $expansionHtml[] =          $fieldWizardHtml;
+        $expansionHtml[] =      '</div>';
+        $expansionHtml[] =  '</div>';
+        $expansionHtml[] = '</div>';
+        $expansionHtml = implode(LF, $expansionHtml);
+
+        $fullElement = $expansionHtml;
+        if ($this->hasNullCheckboxButNoPlaceholder()) {
+            $checked = $itemValue !== null ? ' checked="checked"' : '';
+            $fullElement = [];
+            $fullElement[] = '<div class="t3-form-field-disable"></div>';
+            $fullElement[] = '<div class="checkbox t3-form-field-eval-null-checkbox">';
+            $fullElement[] =     '<label>';
+            $fullElement[] =         '<input type="hidden"' . $nullControlNameAttribute . ' value="0" />';
+            $fullElement[] =         '<input type="checkbox"' . $nullControlNameAttribute . ' value="1"' . $checked . ' />';
+            $fullElement[] =         $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
+            $fullElement[] =     '</label>';
+            $fullElement[] = '</div>';
+            $fullElement[] = $expansionHtml;
+            $fullElement = implode(LF, $fullElement);
+        } elseif ($this->hasNullCheckboxWithPlaceholder()) {
+            $checked = $itemValue !== null ? ' checked="checked"' : '';
+            $placeholder = $shortenedPlaceholder = $config['placeholder'] ?? '';
+            $disabled = '';
+            $fallbackValue = 0;
+            if (strlen($placeholder) > 0) {
+                $shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
+                if ($placeholder !== $shortenedPlaceholder) {
+                    $overrideLabel = sprintf(
+                        $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
+                        '<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
+                    );
+                } else {
+                    $overrideLabel = sprintf(
+                        $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
+                        htmlspecialchars($placeholder)
+                    );
+                }
+            } else {
+                $fallbackValue = 1;
+                $checked = ' checked="checked"';
+                $disabled = ' disabled="disabled"';
+                $overrideLabel = $languageService->sL(
+                    'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
+                );
+            }
+            $fullElement = [];
+            $fullElement[] = '<div class="checkbox t3js-form-field-eval-null-placeholder-checkbox">';
+            $fullElement[] =     '<label>';
+            $fullElement[] =         '<input type="hidden"' . $nullControlNameAttribute . ' value="' . $fallbackValue . '" />';
+            $fullElement[] =         '<input type="checkbox"' . $nullControlNameAttribute . ' value="1"' . $checked . $disabled . ' />';
+            $fullElement[] =         $overrideLabel;
+            $fullElement[] =     '</label>';
+            $fullElement[] = '</div>';
+            $fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
+            $fullElement[] =    '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
+            $fullElement[] =        '<input type="text" class="form-control" disabled="disabled" value="' . $shortenedPlaceholder . '" />';
+            $fullElement[] =    '</div>';
+            $fullElement[] = '</div>';
+            $fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
+            $fullElement[] =    $expansionHtml;
+            $fullElement[] = '</div>';
+            $fullElement = implode(LF, $fullElement);
+        }
+
+        $resultArray['html'] = '<div class="t3js-formengine-field-item">' . $fieldInformationHtml . $fullElement . '</div>';
+        return $resultArray;
+    }
+
+    /**
+     * @return LanguageService
+     */
+    protected function getLanguageService()
+    {
+        return $GLOBALS['LANG'];
+    }
+}
index bbaeae2..c147ad6 100644 (file)
@@ -14,206 +14,290 @@ namespace TYPO3\CMS\Backend\Form\Element;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Backend\Utility\BackendUtility;
-use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
+use TYPO3\CMS\Lang\LanguageService;
 
 /**
- * Generation of TCEform elements of the type "input type=text"
+ * General type=input element.
+ *
+ * This one kicks in if no specific renderType like "inputDateTime"
+ * or "inputColorPicker" is set.
  */
 class InputTextElement extends AbstractFormElement
 {
     /**
+     * Default field wizards enabled for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldWizard = [
+        'otherLanguageContent' => [
+            'renderType' => 'otherLanguageContent',
+        ],
+        'defaultLanguageDifferences' => [
+            'renderType' => 'defaultLanguageDifferences',
+            'after' => [
+                'otherLanguageContent',
+            ],
+        ],
+    ];
+
+    /**
      * This will render a single-line input form field, possibly with various control/validation features
      *
      * @return array As defined in initializeResultArray() of AbstractNode
      */
     public function render()
     {
+        $languageService = $this->getLanguageService();
+
         $table = $this->data['tableName'];
         $fieldName = $this->data['fieldName'];
         $row = $this->data['databaseRow'];
         $parameterArray = $this->data['parameterArray'];
         $resultArray = $this->initializeResultArray();
-        $isDateField = false;
 
+        $itemValue = $parameterArray['itemFormElValue'];
         $config = $parameterArray['fieldConf']['config'];
-        $specConf = BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras']);
-        $size = MathUtility::forceIntegerInRange($config['size'] ?: $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth);
         $evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
-        $classes = [];
-        $attributes = [];
+        $size = MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth);
+        $width = (int)$this->formMaxWidth($size);
+        $nullControlNameAttribute = ' name="' . htmlspecialchars('control[active][' . $table . '][' . $row['uid'] . '][' . $fieldName . ']') . '"';
 
-        // readonly
         if ($config['readOnly']) {
-            $itemFormElValue = $parameterArray['itemFormElValue'];
-            if (in_array('date', $evalList)) {
-                $config['format'] = 'date';
-            } elseif (in_array('datetime', $evalList)) {
-                $config['format'] = 'datetime';
-            } elseif (in_array('time', $evalList)) {
-                $config['format'] = 'time';
-            }
-            if (in_array('password', $evalList)) {
-                $itemFormElValue = $itemFormElValue ? '*********' : '';
+            // Early return for read only fields
+            if (in_array('password', $evalList, true)) {
+                $itemValue = $itemValue ? '*********' : '';
             }
-            $options = $this->data;
-            $options['parameterArray'] = [
-                'fieldConf' => [
-                    'config' => $config,
-                ],
-                'itemFormElValue' => $itemFormElValue,
-            ];
-            $options['renderType'] = 'none';
-            return $this->nodeFactory->create($options)->render();
-        }
-
-        if (in_array('datetime', $evalList, true)
-            || in_array('date', $evalList)) {
-            $classes[] = 't3js-datetimepicker';
-            $isDateField = true;
-            if (in_array('datetime', $evalList)) {
-                $attributes['data-date-type'] = 'datetime';
-            } elseif (in_array('date', $evalList)) {
-                $attributes['data-date-type'] = 'date';
-            }
-
-            // convert timestamp to proper ISO-8601 date so we get rid of timezone issues on the client.
-            // This only handles integer timestamps; if the field is a date(time), it already was converted to an
-            // ISO-8601 date by DatabaseRowDateTimeFields.
-            if (MathUtility::canBeInterpretedAsInteger($parameterArray['itemFormElValue']) && $parameterArray['itemFormElValue'] != 0) {
-                // output date as a ISO-8601 date; the stored value is the server time zone, so we need to treat it as such.
-                $timestamp = $parameterArray['itemFormElValue'];
-                $timestamp += date('Z', $timestamp);
-                $parameterArray['itemFormElValue'] = gmdate('c', $timestamp);
-            }
-
-            if (isset($config['range']['lower'])) {
-                $attributes['data-date-minDate'] = (int)$config['range']['lower'];
-            }
-            if (isset($config['range']['upper'])) {
-                $attributes['data-date-maxDate'] = (int)$config['range']['upper'];
-            }
-        } elseif (in_array('time', $evalList)) {
-            $isDateField = true;
-            $classes[] = 't3js-datetimepicker';
-            $attributes['data-date-type'] = 'time';
-        } elseif (in_array('timesec', $evalList)) {
-            $isDateField = true;
-            $classes[] = 't3js-datetimepicker';
-            $attributes['data-date-type'] = 'timesec';
+            $html = [];
+            $html[] = '<div class="t3js-formengine-field-item">';
+            $html[] =   '<div class="form-wizards-wrap">';
+            $html[] =       '<div class="form-wizards-element">';
+            $html[] =           '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
+            $html[] =               '<input class="form-control" value="' . htmlspecialchars($itemValue) . '" type="text" disabled>';
+            $html[] =           '</div>';
+            $html[] =       '</div>';
+            $html[] =   '</div>';
+            $html[] = '</div>';
+            $resultArray['html'] = implode(LF, $html);
+            return $resultArray;
         }
 
         // @todo: The whole eval handling is a mess and needs refactoring
         foreach ($evalList as $func) {
-            switch ($func) {
-                case 'required':
-                    $attributes['data-formengine-validation-rules'] = $this->getValidationDataAsJsonString(['required' => true]);
-                    break;
-                default:
-                    // @todo: This is ugly: The code should find out on it's own whether a eval definition is a
-                    // @todo: keyword like "date", or a class reference. The global registration could be dropped then
-                    // Pair hook to the one in \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval()
-                    if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
-                        if (class_exists($func)) {
-                            $evalObj = GeneralUtility::makeInstance($func);
-                            if (method_exists($evalObj, 'deevaluateFieldValue')) {
-                                $_params = [
-                                    'value' => $parameterArray['itemFormElValue']
-                                ];
-                                $parameterArray['itemFormElValue'] = $evalObj->deevaluateFieldValue($_params);
-                            }
-                        }
+            // @todo: This is ugly: The code should find out on it's own whether a eval definition is a
+            // @todo: keyword like "date", or a class reference. The global registration could be dropped then
+            // Pair hook to the one in \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval()
+            if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
+                if (class_exists($func)) {
+                    $evalObj = GeneralUtility::makeInstance($func);
+                    if (method_exists($evalObj, 'deevaluateFieldValue')) {
+                        $_params = [
+                            'value' => $itemValue
+                        ];
+                        $itemValue = $evalObj->deevaluateFieldValue($_params);
                     }
+                    if (method_exists($evalObj, 'returnFieldJS')) {
+                        $resultArray['additionalJavaScriptPost'][] = 'TBE_EDITOR.customEvalFunctions[' . GeneralUtility::quoteJSvalue($func) . ']'
+                            . ' = function(value) {' . $evalObj->returnFieldJS() . '};';
+                    }
+                }
             }
         }
-        $paramsList = [
-            'field' => $parameterArray['itemFormElName'],
-            'evalList' => implode(',', $evalList),
-            'is_in' => trim($config['is_in']),
+
+        $attributes = [
+            'value' => '',
+            'id' => StringUtility::getUniqueId('formengine-input-'),
+            'class' => implode(' ', [
+                'form-control',
+                't3js-clearable',
+                'hasDefaultValue',
+            ]),
+            'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
+            'data-formengine-input-params' => json_encode([
+                'field' => $parameterArray['itemFormElName'],
+                'evalList' => implode(',', $evalList),
+                'is_in' => trim($config['is_in'])
+            ]),
+            'data-formengine-input-name' => $parameterArray['itemFormElName'],
         ];
-        // set classes
-        $classes[] = 'form-control';
-        $classes[] = 't3js-clearable';
-        $classes[] = 'hasDefaultValue';
-
-        // calculate attributes
-        $attributes['data-formengine-validation-rules'] = $this->getValidationDataAsJsonString($config);
-        $attributes['data-formengine-input-params'] = json_encode($paramsList);
-        $attributes['data-formengine-input-name'] = htmlspecialchars($parameterArray['itemFormElName']);
-        $attributes['id'] = StringUtility::getUniqueId('formengine-input-');
-        $attributes['value'] = '';
-        if (isset($config['max']) && (int)$config['max'] > 0) {
-            $attributes['maxlength'] = (int)$config['max'];
-        }
-        if (!empty($classes)) {
-            $attributes['class'] = implode(' ', $classes);
-        }
 
-        // This is the EDITABLE form field.
+        $maxLength = $config['max'] ?? 0;
+        if ((int)$maxLength > 0) {
+            $attributes['maxlength'] = (int)$maxLength;
+        }
         if (!empty($config['placeholder'])) {
             $attributes['placeholder'] = trim($config['placeholder']);
         }
-
         if (isset($config['autocomplete'])) {
             $attributes['autocomplete'] = empty($config['autocomplete']) ? 'new-' . $fieldName : 'on';
         }
 
-        // Build the attribute string
-        $attributeString = '';
-        foreach ($attributes as $attributeName => $attributeValue) {
-            $attributeString .= ' ' . $attributeName . '="' . htmlspecialchars($attributeValue) . '"';
+        $valuePickerHtml = [];
+        if (isset($config['valuePicker']['items']) && is_array($config['valuePicker']['items'])) {
+            $mode = $config['valuePicker']['mode'] ?? '';
+            $itemName = $parameterArray['itemFormElName'];
+            $fieldChangeFunc = $parameterArray['fieldChangeFunc'];
+            if ($mode === 'append') {
+                $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
+                    . '.value=\'\'+this.options[this.selectedIndex].value+document.editform[' . GeneralUtility::quoteJSvalue($itemName) . '].value';
+            } elseif ($mode === 'prepend') {
+                $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
+                    . '.value+=\'\'+this.options[this.selectedIndex].value';
+            } else {
+                $assignValue = 'document.querySelectorAll(' . GeneralUtility::quoteJSvalue('[data-formengine-input-name="' . $itemName . '"]') . ')[0]'
+                    . '.value=this.options[this.selectedIndex].value';
+            }
+            $valuePickerHtml[] = '<select';
+            $valuePickerHtml[] =  ' class="form-control tceforms-select tceforms-wizardselect"';
+            $valuePickerHtml[] =  ' onchange="' . htmlspecialchars($assignValue . ';this.blur();this.selectedIndex=0;' . implode('', $fieldChangeFunc)) . '"';
+            $valuePickerHtml[] = '>';
+            $valuePickerHtml[] = '<option></option>';
+            foreach ($config['valuePicker']['items'] as $item) {
+                $valuePickerHtml[] = '<option value="' . htmlspecialchars($item[1]) . '">' . htmlspecialchars($languageService->sL($item[0])) . '</option>';
+            }
+            $valuePickerHtml[] = '</select>';
         }
 
-        $html = '<input type="text"' . $attributeString . ' />';
+        $valueSliderHtml = [];
+        if (isset($config['slider']) && is_array($config['slider'])) {
+            $resultArray['requireJsModules'][] = 'TYPO3/CMS/Backend/ValueSlider';
+            $min = $config['range']['lower'] ?? 0;
+            $max = $config['range']['upper'] ?? 10000;
+            $step = $config['slider']['step'] ?? 1;
+            $width = $config['slider']['width'] ?? 400;
+            $valueType = 'null';
+            if (in_array('int', $evalList, true)) {
+                $valueType = 'int';
+                $itemValue = (int)$itemValue;
+            } elseif (in_array('double2', $evalList, true)) {
+                $valueType = 'double';
+                $itemValue = (double)$itemValue;
+            }
+            $callbackParams = [ $table, $row['uid'], $fieldName, $parameterArray['itemFormElName'] ];
+            $id = 'slider-' . md5($parameterArray['itemFormElName']);
+            $valueSliderHtml[] = '<div';
+            $valueSliderHtml[] =    ' id="' . $id . '"';
+            $valueSliderHtml[] =    ' data-slider-id="' . $id . '"';
+            $valueSliderHtml[] =    ' data-slider-min="' . (int)$min . '"';
+            $valueSliderHtml[] =    ' data-slider-max="' . (int)$max . '"';
+            $valueSliderHtml[] =    ' data-slider-step="' . htmlspecialchars($step) . '"';
+            $valueSliderHtml[] =    ' data-slider-value="' . htmlspecialchars($itemValue) . '"';
+            $valueSliderHtml[] =    ' data-slider-value-type="' . htmlspecialchars($valueType) . '"';
+            $valueSliderHtml[] =    ' data-slider-item-name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
+            $valueSliderHtml[] =    ' data-slider-callback-params="' . htmlspecialchars(json_encode($callbackParams)) . '"';
+            $valueSliderHtml[] =    ' style="width: ' . $width . 'px;"';
+            $valueSliderHtml[] = '>';
+            $valueSliderHtml[] = '</div>';
+        }
 
-        // This is the ACTUAL form field - values from the EDITABLE field must be transferred to this field which is the one that is written to the database.
-        $html .= '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($parameterArray['itemFormElValue']) . '" />';
+        $legacyWizards = $this->renderWizards();
+        $legacyFieldControlHtml = implode(LF, $legacyWizards['fieldControl']);
+        $legacyFieldWizardHtml = implode(LF, $legacyWizards['fieldWizard']);
 
-        // Going through all custom evaluations configured for this field
-        // @todo: Similar to above code!
-        foreach ($evalList as $evalData) {
-            if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$evalData])) {
-                if (class_exists($evalData)) {
-                    $evalObj = GeneralUtility::makeInstance($evalData);
-                    if (method_exists($evalObj, 'returnFieldJS')) {
-                        $resultArray['additionalJavaScriptPost'][] = 'TBE_EDITOR.customEvalFunctions[' . GeneralUtility::quoteJSvalue($evalData) . '] = function(value) {' . $evalObj->returnFieldJS() . '};';
-                    }
+        $fieldInformationResult = $this->renderFieldInformation();
+        $fieldInformationHtml = $fieldInformationResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
+
+        $fieldControlResult = $this->renderFieldControl();
+        $fieldControlHtml = $legacyFieldControlHtml . $fieldControlResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
+
+        $fieldWizardResult = $this->renderFieldWizard();
+        $fieldWizardHtml = $legacyFieldWizardHtml . $fieldWizardResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
+
+        $mainFieldHtml = [];
+        $mainFieldHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
+        $mainFieldHtml[] =  '<div class="form-wizards-wrap">';
+        $mainFieldHtml[] =      '<div class="form-wizards-element">';
+        $mainFieldHtml[] =          '<input type="text"' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
+        $mainFieldHtml[] =          '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($itemValue) . '" />';
+        $mainFieldHtml[] =      '</div>';
+        $mainFieldHtml[] =      '<div class="form-wizards-items-aside">';
+        $mainFieldHtml[] =          '<div class="btn-group">';
+        $mainFieldHtml[] =              implode(LF, $valuePickerHtml);
+        $mainFieldHtml[] =              implode(LF, $valueSliderHtml);
+        $mainFieldHtml[] =              $fieldControlHtml;
+        $mainFieldHtml[] =          '</div>';
+        $mainFieldHtml[] =      '</div>';
+        $mainFieldHtml[] =      '<div class="form-wizards-items-bottom">';
+        $mainFieldHtml[] =          $fieldWizardHtml;
+        $mainFieldHtml[] =      '</div>';
+        $mainFieldHtml[] =  '</div>';
+        $mainFieldHtml[] = '</div>';
+        $mainFieldHtml = implode(LF, $mainFieldHtml);
+
+        $fullElement = $mainFieldHtml;
+        if ($this->hasNullCheckboxButNoPlaceholder()) {
+            $checked = $itemValue !== null ? ' checked="checked"' : '';
+            $fullElement = [];
+            $fullElement[] = '<div class="t3-form-field-disable"></div>';
+            $fullElement[] = '<div class="checkbox t3-form-field-eval-null-checkbox">';
+            $fullElement[] =     '<label>';
+            $fullElement[] =         '<input type="hidden"' . $nullControlNameAttribute . ' value="0" />';
+            $fullElement[] =         '<input type="checkbox"' . $nullControlNameAttribute . ' value="1"' . $checked . ' />';
+            $fullElement[] =         $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
+            $fullElement[] =     '</label>';
+            $fullElement[] = '</div>';
+            $fullElement[] = $mainFieldHtml;
+            $fullElement = implode(LF, $fullElement);
+        } elseif ($this->hasNullCheckboxWithPlaceholder()) {
+            $checked = $itemValue !== null ? ' checked="checked"' : '';
+            $placeholder = $shortenedPlaceholder = $config['placeholder'] ?? '';
+            $disabled = '';
+            $fallbackValue = 0;
+            if (strlen($placeholder) > 0) {
+                $shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
+                if ($placeholder !== $shortenedPlaceholder) {
+                    $overrideLabel = sprintf(
+                        $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
+                        '<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
+                    );
+                } else {
+                    $overrideLabel = sprintf(
+                        $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
+                        htmlspecialchars($placeholder)
+                    );
                 }
+            } else {
+                $fallbackValue = 1;
+                $checked = ' checked="checked"';
+                $disabled = ' disabled="disabled"';
+                $overrideLabel = $languageService->sL(
+                    'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
+                );
             }
+            $fullElement = [];
+            $fullElement[] = '<div class="checkbox t3js-form-field-eval-null-placeholder-checkbox">';
+            $fullElement[] =     '<label>';
+            $fullElement[] =         '<input type="hidden"' . $nullControlNameAttribute . ' value="' . $fallbackValue . '" />';
+            $fullElement[] =         '<input type="checkbox"' . $nullControlNameAttribute . ' value="1"' . $checked . $disabled . ' />';
+            $fullElement[] =         $overrideLabel;
+            $fullElement[] =     '</label>';
+            $fullElement[] = '</div>';
+            $fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
+            $fullElement[] =    '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
+            $fullElement[] =        '<input type="text" class="form-control" disabled="disabled" value="' . $shortenedPlaceholder . '" />';
+            $fullElement[] =    '</div>';
+            $fullElement[] = '</div>';
+            $fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
+            $fullElement[] =    $mainFieldHtml;
+            $fullElement[] = '</div>';
+            $fullElement = implode(LF, $fullElement);
         }
 
-        // add HTML wrapper
-        if ($isDateField) {
-            $html = '
-                               <div class="input-group">
-                                       ' . $html . '
-                                       <span class="input-group-btn">
-                                               <label class="btn btn-default" for="' . $attributes['id'] . '">
-                                                       ' . $this->iconFactory->getIcon('actions-edit-pick-date', Icon::SIZE_SMALL)->render() . '
-                                               </label>
-                                       </span>
-                               </div>';
-        }
-
-        // Wrap a wizard around the item?
-        $html = $this->renderWizards(
-            [$html],
-            $config['wizards'],
-            $table,
-            $row,
-            $fieldName,
-            $parameterArray,
-            $parameterArray['itemFormElName'],
-            $specConf
-        );
-
-        // Add a wrapper to remain maximum width
-        $width = (int)$this->formMaxWidth($size);
-        $html = '<div class="form-control-wrap"' . ($width ? ' style="max-width: ' . $width . 'px"' : '') . '>' . $html . '</div>';
-        $resultArray['html'] = $html;
+        $resultArray['html'] = '<div class="t3js-formengine-field-item">' . $fieldInformationHtml . $fullElement . '</div>';
         return $resultArray;
     }
+
+    /**
+     * @return LanguageService
+     */
+    protected function getLanguageService()
+    {
+        return $GLOBALS['LANG'];
+    }
 }
index 5ec92b9..21d0fcb 100644 (file)
@@ -14,159 +14,73 @@ namespace TYPO3\CMS\Backend\Form\Element;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Backend\Utility\BackendUtility;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 
 /**
- * Generation of TCEform elements where no rendering could be found
+ * None element is a "disabled" input element with formatted values if needed.
  */
 class NoneElement extends AbstractFormElement
 {
     /**
+     * Default field wizards enabled for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldWizard = [
+        'otherLanguageContent' => [
+            'renderType' => 'otherLanguageContent',
+        ],
+        'defaultLanguageDifferences' => [
+            'renderType' => 'defaultLanguageDifferences',
+            'after' => [
+                'otherLanguageContent',
+            ],
+        ],
+    ];
+
+    /**
      * This will render a non-editable display of the content of the field.
      *
      * @return string The HTML code for the TCEform field
      */
     public function render()
     {
+        $resultArray = $this->initializeResultArray();
+
         $parameterArray = $this->data['parameterArray'];
         $config = $parameterArray['fieldConf']['config'];
         $itemValue = $parameterArray['itemFormElValue'];
 
-        if ($config['format']) {
-            $itemValue = $this->formatValue($config, $itemValue);
+        if (isset($config['format']) && $config['format']) {
+            $formatOptions = $config['format.'] ?? [];
+            $itemValue = $this->formatValue($config['format'], $itemValue, $formatOptions);
         }
         if (!$config['pass_content']) {
             $itemValue = htmlspecialchars($itemValue);
         }
 
-        $resultArray = $this->initializeResultArray();
-        $rows = (int)$config['rows'];
-        // Render as textarea
-        if ($rows > 1 || $config['type'] === 'text') {
-            $cols = MathUtility::forceIntegerInRange($config['cols'] ?: $this->defaultInputWidth, 5, $this->maxInputWidth);
-            $width = $this->formMaxWidth($cols);
-            $html = '
-                               <div class="form-control-wrap"' . ($width ? ' style="max-width: ' . $width . 'px"' : '') . '>
-                                       <textarea class="form-control" rows="' . $rows . '" disabled>' . $itemValue . '</textarea>
-                               </div>';
-        } else {
-            $cols = $config['cols'] ?: ($config['size'] ?: $this->defaultInputWidth);
-            $size = MathUtility::forceIntegerInRange($cols ?: $this->defaultInputWidth, 5, $this->maxInputWidth);
-            $width = $this->formMaxWidth($size);
-            $html = '
-                               <div class="form-control-wrap"' . ($width ? ' style="max-width: ' . $width . 'px"' : '') . '>
-                                       <input class="form-control" value="' . $itemValue . '" type="text" disabled>
-                               </div>';
-        }
-        $resultArray['html'] = $html;
-        return $resultArray;
-    }
+        $cols = $config['cols'] ?: ($config['size'] ?: $this->defaultInputWidth);
+        $size = MathUtility::forceIntegerInRange($cols ?: $this->defaultInputWidth, 5, $this->maxInputWidth);
+        $width = $this->formMaxWidth($size);
 
-    /**
-     * Format field content if $config['format'] is set to date, filesize, ..., user
-     *
-     * @param array $config Configuration for the display
-     * @param string $itemValue The value to display
-     * @return string Formatted field value
-     */
-    protected function formatValue($config, $itemValue)
-    {
-        $format = trim($config['format']);
-        switch ($format) {
-            case 'date':
-                if ($itemValue) {
-                    $option = isset($config['format.']['option']) ? trim($config['format.']['option']) : '';
-                    if ($option) {
-                        if (isset($config['format.']['strftime']) && $config['format.']['strftime']) {
-                            $value = strftime($option, $itemValue);
-                        } else {
-                            $value = date($option, $itemValue);
-                        }
-                    } else {
-                        $value = date('d-m-Y', $itemValue);
-                    }
-                } else {
-                    $value = '';
-                }
-                if (isset($config['format.']['appendAge']) && $config['format.']['appendAge']) {
-                    $age = BackendUtility::calcAge(
-                        $GLOBALS['EXEC_TIME'] - $itemValue,
-                        $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
-                    );
-                    $value .= ' (' . $age . ')';
-                }
-                $itemValue = $value;
-                break;
-            case 'datetime':
-                // compatibility with "eval" (type "input")
-                if ($itemValue !== '' && !is_null($itemValue)) {
-                    $itemValue = date('H:i d-m-Y', (int)$itemValue);
-                }
-                break;
-            case 'time':
-                // compatibility with "eval" (type "input")
-                if ($itemValue !== '' && !is_null($itemValue)) {
-                    $itemValue = date('H:i', (int)$itemValue);
-                }
-                break;
-            case 'timesec':
-                // compatibility with "eval" (type "input")
-                if ($itemValue !== '' && !is_null($itemValue)) {
-                    $itemValue = date('H:i:s', (int)$itemValue);
-                }
-                break;
-            case 'year':
-                // compatibility with "eval" (type "input")
-                if ($itemValue !== '' && !is_null($itemValue)) {
-                    $itemValue = date('Y', (int)$itemValue);
-                }
-                break;
-            case 'int':
-                $baseArr = ['dec' => 'd', 'hex' => 'x', 'HEX' => 'X', 'oct' => 'o', 'bin' => 'b'];
-                $base = isset($config['format.']['base']) ? trim($config['format.']['base']) : '';
-                $format = isset($baseArr[$base]) ? $baseArr[$base] : 'd';
-                $itemValue = sprintf('%' . $format, $itemValue);
-                break;
-            case 'float':
-                // default precision
-                $precision = 2;
-                if (isset($config['format.']['precision'])) {
-                    $precision = MathUtility::forceIntegerInRange($config['format.']['precision'], 1, 10, $precision);
-                }
-                $itemValue = sprintf('%.' . $precision . 'f', $itemValue);
-                break;
-            case 'number':
-                $format = isset($config['format.']['option']) ? trim($config['format.']['option']) : '';
-                $itemValue = sprintf('%' . $format, $itemValue);
-                break;
-            case 'md5':
-                $itemValue = md5($itemValue);
-                break;
-            case 'filesize':
-                // We need to cast to int here, otherwise empty values result in empty output,
-                // but we expect zero.
-                $value = GeneralUtility::formatSize((int)$itemValue);
-                if (!empty($config['format.']['appendByteSize'])) {
-                    $value .= ' (' . $itemValue . ')';
-                }
-                $itemValue = $value;
-                break;
-            case 'user':
-                $func = trim($config['format.']['userFunc']);
-                if ($func) {
-                    $params = [
-                        'value' => $itemValue,
-                        'args' => $config['format.']['userFunc'],
-                        'config' => $config,
-                    ];
-                    $itemValue = GeneralUtility::callUserFunction($func, $params, $this);
-                }
-                break;
-            default:
-                // Do nothing e.g. when $format === ''
-        }
-        return $itemValue;
+        $fieldInformationResult = $this->renderFieldInformation();
+        $fieldInformationHtml = $fieldInformationResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
+
+        $html = [];
+        $html[] = '<div class="t3js-formengine-field-item">';
+        $html[] =   $fieldInformationHtml;
+        $html[] =   '<div class="form-wizards-wrap">';
+        $html[] =       '<div class="form-wizards-element">';
+        $html[] =           '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
+        $html[] =               '<input class="form-control" value="' . htmlspecialchars($itemValue) . '" type="text" disabled>';
+        $html[] =           '</div>';
+        $html[] =       '</div>';
+        $html[] =   '</div>';
+        $html[] = '</div>';
+
+        $resultArray['html'] = implode(LF, $html);
+
+        return $resultArray;
     }
 }
index 96b6d08..3d75f71 100644 (file)
@@ -20,12 +20,31 @@ namespace TYPO3\CMS\Backend\Form\Element;
 class RadioElement extends AbstractFormElement
 {
     /**
+     * Default field wizards enabled for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldWizard = [
+        'otherLanguageContent' => [
+            'renderType' => 'otherLanguageContent',
+        ],
+        'defaultLanguageDifferences' => [
+            'renderType' => 'defaultLanguageDifferences',
+            'after' => [
+                'otherLanguageContent',
+            ],
+        ],
+    ];
+
+    /**
      * This will render a series of radio buttons.
      *
      * @return array As defined in initializeResultArray() of AbstractNode
      */
     public function render()
     {
+        $resultArray = $this->initializeResultArray();
+
         $disabled = '';
         if ($this->data['parameterArray']['fieldConf']['config']['readOnly']) {
             $disabled = ' disabled';
@@ -37,22 +56,44 @@ class RadioElement extends AbstractFormElement
             $value = $itemLabelAndValue[1];
             $radioId = htmlspecialchars($this->data['parameterArray']['itemFormElID'] . '_' . $itemNumber);
             $radioChecked = (string)$value === (string)$this->data['parameterArray']['itemFormElValue'] ? ' checked="checked"' : '';
-            $html[] = '<div class="radio' . $disabled . '">';
-            $html[] =    '<label for="' . $radioId . '">';
-            $html[] =        '<input type="radio"'
-                                . ' name="' . htmlspecialchars($this->data['parameterArray']['itemFormElName']) . '"'
-                                . ' id="' . $radioId . '"'
-                                . ' value="' . htmlspecialchars($value) . '"'
-                                . $radioChecked
-                                . $disabled
-                                . ' onclick="' . htmlspecialchars(implode('', $this->data['parameterArray']['fieldChangeFunc'])) . '"'
-                            . '/>'
-                            . htmlspecialchars($label);
-            $html[] =    '</label>';
+
+            $fieldInformationResult = $this->renderFieldInformation();
+            $fieldInformationHtml = $fieldInformationResult['html'];
+            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
+
+            $fieldWizardResult = $this->renderFieldWizard();
+            $fieldWizardHtml = $fieldWizardResult['html'];
+            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
+
+            $html[] = '<div class="t3js-formengine-field-item">';
+            if (!$disabled) {
+                $html[] = $fieldInformationHtml;
+            }
+            $html[] =   '<div class="form-wizards-wrap">';
+            $html[] =       '<div class="form-wizards-element">';
+            $html[] =           '<div class="radio' . $disabled . '">';
+            $html[] =               '<label for="' . $radioId . '">';
+            $html[] =                   '<input type="radio"';
+            $html[] =                       ' name="' . htmlspecialchars($this->data['parameterArray']['itemFormElName']) . '"';
+            $html[] =                       ' id="' . $radioId . '"';
+            $html[] =                       ' value="' . htmlspecialchars($value) . '"';
+            $html[] =                       $radioChecked;
+            $html[] =                       $disabled;
+            $html[] =                       ' onclick="' . htmlspecialchars(implode('', $this->data['parameterArray']['fieldChangeFunc'])) . '"';
+            $html[] =                   '/>';
+            $html[] =                       htmlspecialchars($label);
+            $html[] =               '</label>';
+            $html[] =           '</div>';
+            $html[] =       '</div>';
+            if (!$disabled) {
+                $html[] =   '<div class="form-wizards-items-bottom">';
+                $html[] =       $fieldWizardHtml;
+                $html[] =   '</div>';
+            }
+            $html[] =   '</div>';
             $html[] = '</div>';
         }
 
-        $resultArray = $this->initializeResultArray();
         $resultArray['html'] = implode(LF, $html);
         return $resultArray;
     }
index 43b9efc..c20010c 100644 (file)
@@ -28,12 +28,31 @@ use TYPO3\CMS\Core\Utility\StringUtility;
 class SelectCheckBoxElement extends AbstractFormElement
 {
     /**
+     * Default field wizards enabled for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldWizard = [
+        'otherLanguageContent' => [
+            'renderType' => 'otherLanguageContent',
+        ],
+        'defaultLanguageDifferences' => [
+            'renderType' => 'defaultLanguageDifferences',
+            'after' => [
+                'otherLanguageContent',
+            ],
+        ],
+    ];
+
+    /**
      * Render check boxes
      *
      * @return array As defined in initializeResultArray() of AbstractNode
      */
     public function render()
     {
+        $resultArray = $this->initializeResultArray();
+
         $html = [];
         // Field configuration from TCA:
         $parameterArray = $this->data['parameterArray'];
@@ -110,6 +129,25 @@ class SelectCheckBoxElement extends AbstractFormElement
                     }
                 }
             }
+
+            $legacyWizards = $this->renderWizards();
+            $legacyFieldWizardHtml = implode(LF, $legacyWizards['fieldWizard']);
+
+            $fieldInformationResult = $this->renderFieldInformation();
+            $fieldInformationHtml = $fieldInformationResult['html'];
+            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
+
+            $fieldWizardResult = $this->renderFieldWizard();
+            $fieldWizardHtml = $legacyFieldWizardHtml . $fieldWizardResult['html'];
+            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
+
+            $html[] = '<div class="t3js-formengine-field-item">';
+            if (!$disabled) {
+                $html[] = $fieldInformationHtml;
+            }
+            $html[] =   '<div class="form-wizards-wrap">';
+            $html[] =       '<div class="form-wizards-element">';
+
             // Add an empty hidden field which will send a blank value if all items are unselected.
             $html[] = '<input type="hidden" class="select-checkbox" name="' . htmlspecialchars($parameterArray['itemFormElName']) . '" value="">';
 
@@ -190,25 +228,19 @@ class SelectCheckBoxElement extends AbstractFormElement
                 }
                 $html[] = '</div>';
             }
-        }
 
-        if (!$disabled) {
-            $html = $this->renderWizards(
-                [implode(LF, $html)],
-                $config['wizards'],
-                $this->data['tableName'],
-                $this->data['databaseRow'],
-                $this->data['fieldName'],
-                $parameterArray,
-                $parameterArray['itemFormElName'],
-                BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras'])
-            );
+            $html[] =       '</div>';
+            if (!$disabled) {
+                $html[] =   '<div class="form-wizards-items-bottom">';
+                $html[] =       $fieldWizardHtml;
+                $html[] =   '</div>';
+            }
+            $html[] =   '</div>';
+            $html[] = '</div>';
         }
 
-        $resultArray = $this->initializeResultArray();
-        $resultArray['html'] = $html;
+        $resultArray['html'] = implode(LF, $html);
         $resultArray['requireJsModules'][] = 'TYPO3/CMS/Backend/Tooltip';
-
         return $resultArray;
     }
 }
index dfe6089..0954b77 100644 (file)
@@ -14,7 +14,6 @@ namespace TYPO3\CMS\Backend\Form\Element;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Utility\MathUtility;
@@ -29,6 +28,45 @@ use TYPO3\CMS\Lang\LanguageService;
 class SelectMultipleSideBySideElement extends AbstractFormElement
 {
     /**
+     * Default field controls for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldControl = [
+        'editPopup' => [
+            'renderType' => 'editPopup',
+            'disabled' => true,
+        ],
+        'addRecord' => [
+            'renderType' => 'addRecord',
+            'disabled' => true,
+            'after' => [ 'editPopup' ],
+        ],
+        'listModule' => [
+            'renderType' => 'listModule',
+            'disabled' => true,
+            'after' => [ 'addRecord' ],
+        ],
+    ];
+
+    /**
+     * Default field wizards enabled for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldWizard = [
+        'otherLanguageContent' => [
+            'renderType' => 'otherLanguageContent',
+        ],
+        'defaultLanguageDifferences' => [
+            'renderType' => 'defaultLanguageDifferences',
+            'after' => [
+                'otherLanguageContent',
+            ],
+        ],
+    ];
+
+    /**
      * Render side by side element.
      *
      * @return array As defined in initializeResultArray() of AbstractNode
@@ -36,6 +74,7 @@ class SelectMultipleSideBySideElement extends AbstractFormElement
     public function render()
     {
         $languageService = $this->getLanguageService();
+        $resultArray = $this->initializeResultArray();
 
         $parameterArray = $this->data['parameterArray'];
         $config = $parameterArray['fieldConf']['config'];
@@ -156,108 +195,127 @@ class SelectMultipleSideBySideElement extends AbstractFormElement
             $selectableListStyle = ' style="' . htmlspecialchars($config['itemListStyle']) . '"';
         }
 
+        $legacyWizards = $this->renderWizards();
+        $legacyFieldControlHtml = implode(LF, $legacyWizards['fieldControl']);
+        $legacyFieldWizardHtml = implode(LF, $legacyWizards['fieldWizard']);
+
+        $fieldInformationResult = $this->renderFieldInformation();
+        $fieldInformationHtml = $fieldInformationResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
+
+        $fieldControlResult = $this->renderFieldControl();
+        $fieldControlHtml = $legacyFieldControlHtml . $fieldControlResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
+
+        $fieldWizardResult = $this->renderFieldWizard();
+        $fieldWizardHtml = $legacyFieldWizardHtml . $fieldWizardResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
+
         $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">';
+        $html[] = '<div class="t3js-formengine-field-item">';
+        $html[] =   $fieldInformationHtml;
+        $html[] =   '<div class="form-wizards-wrap">';
+        $html[] =       '<div class="form-wizards-element">';
+        $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-aside">';
+        $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>';
+            $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>';
+            $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-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[] =                               '<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[] =                       ' data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString($config)) . '"';
+        $html[] =                       $selectableListStyle;
         $html[] =                   '>';
-        $html[] =                       $this->iconFactory->getIcon('actions-selection-delete', Icon::SIZE_SMALL)->render();
-        $html[] =                   '</a>';
+        $html[] =                       implode(LF, $selectableItemsHtml);
+        $html[] =                   '</select>';
         $html[] =               '</div>';
         $html[] =           '</div>';
+        $html[] =           '<input type="hidden" name="' . htmlspecialchars($elementName) . '" value="' . htmlspecialchars(implode(',', $listOfSelectedValues)) . '" />';
+        $html[] =       '</div>';
+        $html[] =       '<div class="form-wizards-items-aside">';
+        $html[] =           '<div class="btn-group-vertical">';
+        $html[] =               $fieldControlHtml;
+        $html[] =           '</div>';
+        $html[] =       '</div>';
+        $html[] =       '<div class="form-wizards-items-bottom">';
+        $html[] =           $fieldWizardHtml;
         $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['html'] = implode(LF, $html);
         return $resultArray;
     }
 
@@ -291,10 +349,6 @@ class SelectMultipleSideBySideElement extends AbstractFormElement
         if ($size !== 1) {
             $multiple = ' multiple="multiple"';
         }
-        $style = '';
-        if (isset($config['selectedListStyle'])) {
-            $style = ' style="' . htmlspecialchars($config['selectedListStyle']) . '"';
-        }
 
         $listOfSelectedValues = [];
         $optionsHtml = [];
@@ -310,25 +364,30 @@ class SelectMultipleSideBySideElement extends AbstractFormElement
         }
 
         $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 class="t3js-formengine-field-item">';
+        $html[] =   '<div class="form-wizards-wrap">';
+        $html[] =       '<div class="form-wizards-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="form-control tceforms-multiselect"';
+        $html[] =                       $multiple;
+        $html[] =                       ' data-formengine-input-name="' . htmlspecialchars($fieldName) . '"';
+        $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)) . '" />';
+        $html[] =       '</div>';
         $html[] =   '</div>';
         $html[] = '</div>';
-        $html[] = '<input type="hidden" name="' . htmlspecialchars($fieldName) . '" value="' . htmlspecialchars(implode(',', $listOfSelectedValues)) . '" />';
 
         $resultArray = $this->initializeResultArray();
         $resultArray['html'] = implode(LF, $html);
index 5d0400b..94a6f69 100644 (file)
@@ -14,8 +14,6 @@ namespace TYPO3\CMS\Backend\Form\Element;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Backend\Utility\BackendUtility;
-use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
@@ -28,87 +26,119 @@ use TYPO3\CMS\Core\Utility\StringUtility;
 class SelectSingleBoxElement extends AbstractFormElement
 {
     /**
+     * Default field controls for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldControl = [
+        'resetSelection' => [
+            'renderType' => 'resetSelection',
+        ],
+    ];
+
+    /**
+     * Default field wizards enabled for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldWizard = [
+        'otherLanguageContent' => [
+            'renderType' => 'otherLanguageContent',
+        ],
+        'defaultLanguageDifferences' => [
+            'renderType' => 'defaultLanguageDifferences',
+            'after' => [
+                'otherLanguageContent',
+            ],
+        ],
+    ];
+
+    /**
      * This will render a selector box element, or possibly a special construction with two selector boxes.
      *
      * @return array As defined in initializeResultArray() of AbstractNode
      */
     public function render()
     {
+        $languageService = $this->getLanguageService();
+        $resultArray = $this->initializeResultArray();
+
         $parameterArray = $this->data['parameterArray'];
         // Field configuration from TCA:
         $config = $parameterArray['fieldConf']['config'];
         $selectItems = $parameterArray['fieldConf']['config']['items'];
+        $disabled = !empty($config['readOnly']);
 
         // Get values in an array (and make unique, which is fine because there can be no duplicates anyway):
         $itemArray = array_flip($parameterArray['itemFormElValue']);
-        $optionElements = [];
-        $initiallySelectedIndices = [];
+        $width = $this->formMaxWidth($this->defaultInputWidth);
 
+        $optionElements = [];
         foreach ($selectItems as $i => $item) {
             $value = $item[1];
             $attributes = [];
-
             // Selected or not by default
             if (isset($itemArray[$value])) {
                 $attributes['selected'] = 'selected';
-                $initiallySelectedIndices[] = $i;
                 unset($itemArray[$value]);
             }
-
             // Non-selectable element
             if ((string)$value === '--div--') {
                 $attributes['disabled'] = 'disabled';
                 $attributes['class'] = 'formcontrol-select-divider';
             }
-
             $optionElements[] = $this->renderOptionElement($value, $item[0], $attributes);
         }
 
         $selectElement = $this->renderSelectElement($optionElements, $parameterArray, $config);
-        $resetButtonElement = $this->renderResetButtonElement($parameterArray['itemFormElName'] . '[]', $initiallySelectedIndices);
-        $html = [];
 
-        // Add an empty hidden field which will send a blank value if all items are unselected.
-        if (empty($config['readOnly'])) {
-            $html[] = '<input type="hidden" name="' . htmlspecialchars($parameterArray['itemFormElName']) . '" value="">';
-        }
+        $legacyWizards = $this->renderWizards();
+        $legacyFieldControlHtml = implode(LF, $legacyWizards['fieldControl']);
+        $legacyFieldWizardHtml = implode(LF, $legacyWizards['fieldWizard']);
 
-        // Put it all together
-        $width = $this->formMaxWidth($this->defaultInputWidth);
-        $html = array_merge($html, [
-            '<div class="form-control-wrap" ' . ($width ? ' style="max-width: ' . $width . 'px"' : '') . '>',
-                '<div class="form-wizards-wrap form-wizards-aside">',
-                    '<div class="form-wizards-element">',
-                        $selectElement,
-                    '</div>',
-                    '<div class="form-wizards-items">',
-                        $resetButtonElement,
-                    '</div>',
-                '</div>',
-            '</div>',
-            '<p>',
-                '<em>' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.holdDownCTRL')) . '</em>',
-            '</p>',
-        ]);
-        $html = implode(LF, $html);
-
-        // Wizards:
-        if (empty($config['readOnly'])) {
-            $html = $this->renderWizards(
-                [$html],
-                $config['wizards'],
-                $this->data['tableName'],
-                $this->data['databaseRow'],
-                $this->data['fieldName'],
-                $parameterArray,
-                $parameterArray['itemFormElName'],
-                BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras'])
-            );
-        }
+        $fieldInformationResult = $this->renderFieldInformation();
+        $fieldInformationHtml = $fieldInformationResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
 
-        $resultArray = $this->initializeResultArray();
-        $resultArray['html'] = $html;
+        $fieldControlResult = $this->renderFieldControl();
+        $fieldControlHtml = $legacyFieldControlHtml . $fieldControlResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
 
+        $fieldWizardResult = $this->renderFieldWizard();
+        $fieldWizardHtml = $legacyFieldWizardHtml . $fieldWizardResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
+
+        $html = [];
+        $html[] = '<div class="t3js-formengine-field-item">';
+        if (!$disabled) {
+            $html[] = $fieldInformationHtml;
+        }
+        $html[] =   '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
+        $html[] =       '<div class="form-wizards-wrap form-wizards-aside">';
+        $html[] =           '<div class="form-wizards-element">';
+        if (!$disabled) {
+            // Add an empty hidden field which will send a blank value if all items are unselected.
+            $html[] =           '<input type="hidden" name="' . htmlspecialchars($parameterArray['itemFormElName']) . '" value="">';
+        }
+        $html[] =               $selectElement;
+        $html[] =           '</div>';
+        if (!$disabled) {
+            $html[] =       '<div class="form-wizards-items-aside">';
+            $html[] =           $fieldControlHtml;
+            $html[] =       '</div>';
+            $html[] =   '</div>';
+
+            $html[] =   '<p>';
+            $html[] =       '<em>' . htmlspecialchars($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.holdDownCTRL')) . '</em>';
+            $html[] =   '</p>';
+            $html[] =   '<div class="form-wizards-items-bottom">';
+            $html[] =       $fieldWizardHtml;
+            $html[] =   '</div>';
+        }
+        $html[] =   '</div>';
+        $html[] = '</div>';
+
+        $resultArray['html'] = implode(LF, $html);
         return $resultArray;
     }
 
@@ -140,25 +170,22 @@ class SelectSingleBoxElement extends AbstractFormElement
             'onchange' => implode('', $parameterArray['fieldChangeFunc']),
             'id' => StringUtility::getUniqueId($cssPrefix),
             'class' => 'form-control ' . $cssPrefix,
+            'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
         ];
-
         if ($size) {
             $attributes['size'] = $size;
         }
-
         if ($config['readOnly']) {
             $attributes['disabled'] = 'disabled';
         }
-
         if (isset($config['itemListStyle'])) {
             $attributes['style'] = $config['itemListStyle'];
         }
 
-        $html = [
-            '<select ' . $this->implodeAttributes($attributes) . ' ' . $this->getValidationDataAsDataAttribute($config) . '>',
-                implode(LF, $optionElements),
-            '</select>',
-        ];
+        $html = [];
+        $html[] = '<select ' . GeneralUtility::implodeAttributes($attributes, true) . '>';
+        $html[] =   implode(LF, $optionElements);
+        $html[] = '</select>';
 
         return implode(LF, $html);
     }
@@ -173,8 +200,9 @@ class SelectSingleBoxElement extends AbstractFormElement
      */
     protected function renderOptionElement($value, $label, array $attributes = [])
     {
+        $attributes['value'] = $value;
         $html = [
-            '<option value="' . htmlspecialchars($value) . '" ' . $this->implodeAttributes($attributes) . '>',
+            '<option ' . GeneralUtility::implodeAttributes($attributes, true) . '>',
                 htmlspecialchars($label, ENT_COMPAT, 'UTF-8', false),
             '</option>'
 
@@ -182,56 +210,4 @@ class SelectSingleBoxElement extends AbstractFormElement
 
         return implode('', $html);
     }
-
-    /**
-     * Renders a button for resetting to the selection on initial load
-     *
-     * @param string $formElementName Form element name
-     * @param array $initiallySelectedIndices List of initially selected option indices
-     * @return string
-     */
-    protected function renderResetButtonElement($formElementName, array $initiallySelectedIndices)
-    {
-        $formElementName = GeneralUtility::quoteJSvalue($formElementName);
-        $resetCode = [
-            'document.editform[' . $formElementName . '].selectedIndex=-1'
-        ];
-        foreach ($initiallySelectedIndices as $index) {
-            $resetCode[] = 'document.editform[' . $formElementName . '].options[' . $index . '].selected=1';
-        }
-        $resetCode[] = 'return false';
-
-        $attributes = [
-            'href' => '#',
-            'class' => 'btn btn-default',
-            'onclick' => htmlspecialchars(implode(';', $resetCode)),
-            // htmlspecialchars() is done by $this->implodeAttributes()
-            'title' => $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.revertSelection')
-        ];
-
-        $html = [
-            '<a ' . $this->implodeAttributes($attributes) . '>',
-                $this->iconFactory->getIcon('actions-edit-undo', Icon::SIZE_SMALL)->render(),
-            '</a>',
-        ];
-
-        return implode('', $html);
-    }
-
-    /**
-     * Build an HTML attributes string from a map of attributes
-     *
-     * All attribute values are passed through htmlspecialchars()
-     *
-     * @param array $attributes Map of attribute names and values
-     * @return string
-     */
-    protected function implodeAttributes(array $attributes = [])
-    {
-        $html = [];
-        foreach ($attributes as $name => $value) {
-            $html[] = $name . '="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8', false) . '"';
-        }
-        return implode(' ', $html);
-    }
 }
index caf9250..6539009 100644 (file)
@@ -16,7 +16,6 @@ namespace TYPO3\CMS\Backend\Form\Element;
 
 use TYPO3\CMS\Backend\Form\InlineStackProcessor;
 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
-use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
 
@@ -29,12 +28,34 @@ use TYPO3\CMS\Core\Utility\StringUtility;
 class SelectSingleElement extends AbstractFormElement
 {
     /**
+     * Default field wizards enabled for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldWizard = [
+        'selectIcons' => [
+            'renderType' => 'selectIcons',
+            'disabled' => true,
+        ],
+        'otherLanguageContent' => [
+            'renderType' => 'otherLanguageContent',
+            'after' => [ 'selectIcons' ],
+        ],
+        'defaultLanguageDifferences' => [
+            'renderType' => 'defaultLanguageDifferences',
+            'after' => [ 'otherLanguageContent' ],
+        ],
+    ];
+
+    /**
      * Render single element
      *
      * @return array As defined in initializeResultArray() of AbstractNode
      */
     public function render()
     {
+        $resultArray = $this->initializeResultArray();
+
         $table = $this->data['tableName'];
         $field = $this->data['fieldName'];
         $row = $this->data['databaseRow'];
@@ -87,7 +108,6 @@ class SelectSingleElement extends AbstractFormElement
         $selectItemCounter = 0;
         $selectItemGroupCount = 0;
         $selectItemGroups = [];
-        $selectIcons = [];
         $selectedValue = '';
         $hasIcons = false;
 
@@ -118,18 +138,7 @@ class SelectSingleElement extends AbstractFormElement
                     'value' => $item[1],
                     'icon' => $icon,
                     'selected' => $selected,
-                    'index' => $selectItemCounter
                 ];
-
-                // ICON
-                if ($icon) {
-                    $selectIcons[] = [
-                        'title' => $item[0],
-                        'icon' => $icon,
-                        'index' => $selectItemCounter,
-                    ];
-                }
-
                 $selectItemCounter++;
             }
         }
@@ -162,91 +171,60 @@ class SelectSingleElement extends AbstractFormElement
             $options .= ($optionGroup ? '</optgroup>' : '');
         }
 
-        // Build the element
-        $html = ['<div class="form-control-wrap">'];
-
-        if ($hasIcons) {
-            $html[] = '<div class="input-group">';
-            $html[] =    '<span class="input-group-addon input-group-icon">';
-            $html[] =        $selectedIcon;
-            $html[] =    '</span>';
+        $selectAttributes = [
+            'id' => $selectId,
+            'name' => $parameterArray['itemFormElName'],
+            'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
+            'class' => 'form-control form-control-adapt',
+        ];
+        if ($size) {
+            $selectAttributes['size'] = $size;
         }
-
-        $html[] = '<select'
-                    . ' id="' . $selectId . '"'
-                    . ' name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"'
-                    . $this->getValidationDataAsDataAttribute($config)
-                    . ' class="form-control form-control-adapt"'
-                    . ($size ? ' size="' . $size . '"' : '')
-                    . ($disabled ? ' disabled="disabled"' : '')
-                    . '>';
-        $html[] =    $options;
-        $html[] = '</select>';
-
-        if ($hasIcons) {
-            $html[] = '</div>';
+        if ($disabled) {
+            $selectAttributes['disabled'] = 'disabled';
         }
 
-        $html[] = '</div>';
-
-        // Create icon table:
-        if (!empty($selectIcons) && !empty($config['showIconTable'])) {
-            $selectIconColumns = (int)$config['selicon_cols'];
-
-            if (!$selectIconColumns) {
-                $selectIconColumns = count($selectIcons);
-            }
-
-            $selectIconColumns = ($selectIconColumns > 12 ? 12 : $selectIconColumns);
-            $selectIconRows = ceil(count($selectIcons) / $selectIconColumns);
-            $selectIcons = array_pad($selectIcons, $selectIconRows * $selectIconColumns, '');
+        $legacyWizards = $this->renderWizards();
+        $legacyFieldWizardHtml = implode(LF, $legacyWizards['fieldWizard']);
 
-            $html[] = '<div class="t3js-forms-select-single-icons table-icons table-fit table-fit-inline-block">';
-            $html[] =    '<table class="table table-condensed table-white table-center">';
-            $html[] =        '<tbody>';
-            $html[] =            '<tr>';
+        $fieldInformationResult = $this->renderFieldInformation();
+        $fieldInformationHtml = $fieldInformationResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
 
-            foreach ($selectIcons as $i => $selectIcon) {
-                if ($i % $selectIconColumns === 0 && $i !== 0) {
-                    $html[] =    '</tr>';
-                    $html[] =    '<tr>';
-                }
+        $fieldWizardResult = $this->renderFieldWizard();
+        $fieldWizardHtml = $legacyFieldWizardHtml . $fieldWizardResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
 
-                $html[] =            '<td>';
-
-                if (is_array($selectIcon)) {
-                    $html[] = '<a href="#" title="' . htmlspecialchars($selectIcon['title'], ENT_COMPAT, 'UTF-8', false) . '" data-select-index="' . htmlspecialchars($selectIcon['index']) . '">';
-                    $html[] = $selectIcon['icon'];
-                    $html[] = '</a>';
-                }
-
-                $html[] =            '</td>';
-            }
-
-            $html[] =            '</tr>';
-            $html[] =        '</tbody>';
-            $html[] =    '</table>';
-            $html[] = '</div>';
+        $html = [];
+        $html[] = '<div class="t3js-formengine-field-item">';
+        if (!$disabled) {
+            $html[] = $fieldInformationHtml;
         }
-
-        $html = implode(LF, $html);
-
-        // Wizards:
+        $html[] =   '<div class="form-control-wrap">';
+        $html[] =       '<div class="form-wizards-wrap">';
+        $html[] =           '<div class="form-wizards-element">';
+        if ($hasIcons) {
+            $html[] =           '<div class="input-group">';
+            $html[] =               '<span class="input-group-addon input-group-icon">';
+            $html[] =                   $selectedIcon;
+            $html[] =               '</span>';
+        }
+        $html[] =                   '<select ' . GeneralUtility::implodeAttributes($selectAttributes, true) . '>';
+        $html[] =                       $options;
+        $html[] =                   '</select>';
+        if ($hasIcons) {
+            $html[] =           '</div>';
+        }
+        $html[] =           '</div>';
         if (!$disabled) {
-            $html = $this->renderWizards(
-                [$html],
-                $config['wizards'],
-                $table,
-                $row,
-                $field,
-                $parameterArray,
-                $parameterArray['itemFormElName'],
-                BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras'])
-            );
+            $html[] =       '<div class="form-wizards-items-bottom">';
+            $html[] =           $fieldWizardHtml;
+            $html[] =       '</div>';
         }
+        $html[] =       '</div>';
+        $html[] =   '</div>';
+        $html[] = '</div>';
 
-        $resultArray = $this->initializeResultArray();
-        $resultArray['html'] = $html;
         $resultArray['requireJsModules'][] = ['TYPO3/CMS/Backend/FormEngine/Element/SelectSingleElement' => implode(LF, [
             'function(SelectSingleElement) {',
                 'SelectSingleElement.initialize(',
@@ -260,6 +238,7 @@ class SelectSingleElement extends AbstractFormElement
             '}',
         ])];
 
+        $resultArray['html'] = implode(LF, $html);
         return $resultArray;
     }
 }
index 807c55b..fc49a70 100644 (file)
@@ -105,34 +105,45 @@ class SelectTreeElement extends AbstractFormElement
             }
         }
 
+        $fieldInformationResult = $this->renderFieldInformation();
+        $fieldInformationHtml = $fieldInformationResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
+
         $html = [];
-        $html[] = '<div class="typo3-tceforms-tree">';
-        $html[] = '    <input class="treeRecord" type="hidden"';
-        $html[] = '           ' . $this->getValidationDataAsDataAttribute($config);
-        $html[] = '           data-relatedfieldname="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
-        $html[] = '           data-tablename="' . htmlspecialchars($this->data['tableName']) . '"';
-        $html[] = '           data-fieldname="' . htmlspecialchars($this->data['fieldName']) . '"';
-        $html[] = '           data-uid="' . (int)$this->data['vanillaUid'] . '"';
-        $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-tree-expand-up-to-level="' . ($expanded ? '999' : '1') . '"';
-        $html[] = '           data-tree-show-toolbar="' . $showHeader . '"';
-        $html[] = '           name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
-        $html[] = '           id="treeinput' . $formElementId . '"';
-        $html[] = '           value=""';
-        $html[] = '    />';
+        $html[] = '<div class="t3js-formengine-field-item">';
+        if (!$readOnly) {
+            $html[] = $fieldInformationHtml;
+        }
+        $html[] =   '<div class="form-control-wrap">';
+        $html[] =       '<div class="typo3-tceforms-tree">';
+        $html[] =           '<input class="treeRecord" type="hidden"';
+        $html[] =               ' data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString($config)) . '"';
+        $html[] =               ' data-relatedfieldname="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
+        $html[] =               ' data-tablename="' . htmlspecialchars($this->data['tableName']) . '"';
+        $html[] =               ' data-fieldname="' . htmlspecialchars($this->data['fieldName']) . '"';
+        $html[] =               ' data-uid="' . (int)$this->data['vanillaUid'] . '"';
+        $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-tree-expand-up-to-level="' . ($expanded ? '999' : '1') . '"';
+        $html[] =               ' data-tree-show-toolbar="' . $showHeader . '"';
+        $html[] =               ' name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
+        $html[] =               ' id="treeinput' . $formElementId . '"';
+        $html[] =               ' value=""';
+        $html[] =           '/>';
+        $html[] =       '</div>';
+        $html[] =       '<div id="' . $treeWrapperId . '" class="svg-tree-wrapper" style="height: ' . $heightInPx . 'px;"></div>';
+        $html[] =       '<script type="text/javascript">var ' . $treeWrapperId . ' = ' . $this->getTreeOnChangeJs() . '</script>';
+        $html[] =   '</div>';
         $html[] = '</div>';
-        $html[] = '<div id="' . $treeWrapperId . '" class="svg-tree-wrapper" style="height: ' . $heightInPx . 'px;"></div>';
-        $html[] = '<script type="text/javascript">var ' . $treeWrapperId . ' = ' . $this->getTreeOnChangeJs() . '</script>';
 
         $resultArray['html'] = implode(LF, $html);
 
index 8aa0255..52ef143 100644 (file)
@@ -14,11 +14,11 @@ namespace TYPO3\CMS\Backend\Form\Element;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
+use TYPO3\CMS\Lang\LanguageService;
 
 /**
  * Generation of TCEform elements of the type "text"
@@ -26,6 +26,23 @@ use TYPO3\CMS\Core\Utility\StringUtility;
 class TextElement extends AbstractFormElement
 {
     /**
+     * Default field wizards enabled for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldWizard = [
+        'otherLanguageContent' => [
+            'renderType' => 'otherLanguageContent',
+        ],
+        'defaultLanguageDifferences' => [
+            'renderType' => 'defaultLanguageDifferences',
+            'after' => [
+                'otherLanguageContent',
+            ],
+        ],
+    ];
+
+    /**
      * The number of chars expected per row when the height of a text area field is
      * automatically calculated based on the number of characters found in the field content.
      *
@@ -40,28 +57,30 @@ class TextElement extends AbstractFormElement
      */
     public function render()
     {
+        $languageService = $this->getLanguageService();
+        $backendUser = $this->getBackendUserAuthentication();
+
         $table = $this->data['tableName'];
         $fieldName = $this->data['fieldName'];
         $row = $this->data['databaseRow'];
         $parameterArray = $this->data['parameterArray'];
         $resultArray = $this->initializeResultArray();
-        $backendUser = $this->getBackendUserAuthentication();
 
+        $itemValue = $parameterArray['itemFormElValue'];
         $config = $parameterArray['fieldConf']['config'];
-
-        // Setting columns number
+        $evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
         $cols = MathUtility::forceIntegerInRange($config['cols'] ?: $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth);
+        $width = $this->formMaxWidth($cols);
+        $nullControlNameAttribute = ' name="' . htmlspecialchars('control[active][' . $table . '][' . $row['uid'] . '][' . $fieldName . ']') . '"';
 
         // Setting number of rows
         $rows = MathUtility::forceIntegerInRange($config['rows'] ?: 5, 1, 20);
         $originalRows = $rows;
-
-        $itemFormElementValueLength = strlen($parameterArray['itemFormElValue']);
+        $itemFormElementValueLength = strlen($itemValue);
         if ($itemFormElementValueLength > $this->charactersPerRow * 2) {
-            $cols = $this->maxInputWidth;
             $rows = MathUtility::forceIntegerInRange(
                 round($itemFormElementValueLength / $this->charactersPerRow),
-                count(explode(LF, $parameterArray['itemFormElValue'])),
+                count(explode(LF, $itemValue)),
                 20
             );
             if ($rows < $originalRows) {
@@ -69,126 +88,207 @@ class TextElement extends AbstractFormElement
             }
         }
 
-        // must be called after the cols and rows calculation, so the parameters are applied
-        // to read-only fields as well.
-        // @todo: Same as in InputTextElement ...
         if ($config['readOnly']) {
-            $config['cols'] = $cols;
-            $config['rows'] = $rows;
-            $options = $this->data;
-            $options['parameterArray'] = [
-                'fieldConf' => [
-                    'config' => $config,
-                ],
-                'itemFormElValue' => $parameterArray['itemFormElValue'],
-            ];
-            $options['renderType'] = 'none';
-            return $this->nodeFactory->create($options)->render();
+            $html = [];
+            $html[] = '<div class="t3js-formengine-field-item">';
+            $html[] =   '<div class="form-wizards-wrap">';
+            $html[] =       '<div class="form-wizards-element">';
+            $html[] =           '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
+            $html[] =               '<textarea class="form-control" rows="' . $rows . '" disabled>';
+            $html[] =                   htmlspecialchars($itemValue);
+            $html[] =               '</textarea>';
+            $html[] =           '</div>';
+            $html[] =       '</div>';
+            $html[] =   '</div>';
+            $html[] = '</div>';
+            $resultArray['html'] = implode(LF, $html);
+            return $resultArray;
         }
 
-        $evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
-        // "Extra" configuration; Returns configuration for the field based on settings found in the "types" fieldlist. Traditionally, this is where RTE configuration has been found.
-        $specialConfiguration = BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras']);
-        $html = '';
-
-        // Show message, if no RTE (field can only be edited with RTE!)
-        if ($specialConfiguration['rte_only']) {
-            $html = '<p><em>' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.noRTEfound')) . '</em></p>';
-        } else {
-            $attributes = [];
-            // validation
-            foreach ($evalList as $func) {
-                if ($func === 'required') {
-                    $attributes['data-formengine-validation-rules'] = $this->getValidationDataAsJsonString(['required' => true]);
-                } else {
-                    // @todo: This is ugly: The code should find out on it's own whether a eval definition is a
-                    // @todo: keyword like "date", or a class reference. The global registration could be dropped then
-                    // Pair hook to the one in \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval()
-                    // There is a similar hook for "evaluateFieldValue" in DataHandler and InputTextElement
-                    if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
-                        if (class_exists($func)) {
-                            $evalObj = GeneralUtility::makeInstance($func);
-                            if (method_exists($evalObj, 'deevaluateFieldValue')) {
-                                $_params = [
-                                    'value' => $parameterArray['itemFormElValue']
-                                ];
-                                $parameterArray['itemFormElValue'] = $evalObj->deevaluateFieldValue($_params);
-                            }
-                        }
+        // @todo: The whole eval handling is a mess and needs refactoring
+        foreach ($evalList as $func) {
+            // @todo: This is ugly: The code should find out on it's own whether a