From bacaa0a02bf4c322da7828cab5129f1edfb4ecc9 Mon Sep 17 00:00:00 2001 From: Ralf Zimmermann <ralf.zimmermann@tritum.de> Date: Tue, 14 Mar 2017 04:07:25 +0100 Subject: [PATCH] [FEATURE] EXT:form - support multiple form elements per row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make it possible to define multiple form elements per row. The default configuration works for Twitter Bootstrap. Resolves: #80196 Releases: master Change-Id: I28b9f648d2bc202c03b6c6b474f6e975ef1459bd Reviewed-on: https://review.typo3.org/52037 Tested-by: TYPO3com <no-reply@typo3.com> Reviewed-by: Frank Nägler <frank.naegler@typo3.org> Tested-by: Frank Nägler <frank.naegler@typo3.org> Reviewed-by: Bjoern Jacob <bjoern.jacob@tritum.de> Tested-by: Bjoern Jacob <bjoern.jacob@tritum.de> Reviewed-by: Susanne Moog <susanne.moog@typo3.org> Tested-by: Susanne Moog <susanne.moog@typo3.org> --- Build/Resources/Public/Less/form.less | 7 + ...tFormSupportMultipleFormElementsPerRow.rst | 201 ++++++++++++++++++ .../Model/FormElements/GridContainer.php | 90 ++++++++ .../FormElements/GridContainerInterface.php | 23 ++ .../Domain/Model/FormElements/GridRow.php | 103 +++++++++ .../Model/FormElements/GridRowInterface.php | 23 ++ ...ColumnClassAutoConfigurationViewHelper.php | 128 +++++++++++ .../RenderAllFormValuesViewHelper.php | 11 +- .../form/Configuration/Yaml/BaseSetup.yaml | 53 ++++- .../Configuration/Yaml/FormEditorSetup.yaml | 53 +++++ ...GridColumnViewPortConfigurationEditor.html | 17 ++ .../Frontend/Partials/GridContainer.html | 9 + .../Private/Frontend/Partials/GridRow.html | 11 + .../Resources/Private/Language/Database.xlf | 36 ++++ .../sysext/form/Resources/Public/Css/form.css | 9 + .../Resources/Public/Images/gridcontainer.svg | 10 + .../form/Resources/Public/Images/gridrow.svg | 8 + .../Public/JavaScript/Backend/FormEditor.js | 13 ++ .../JavaScript/Backend/FormEditor/Core.js | 39 +++- .../Backend/FormEditor/InspectorComponent.js | 170 ++++++++++++++- .../JavaScript/Backend/FormEditor/Mediator.js | 7 +- .../Backend/FormEditor/ModalsComponent.js | 57 ++++- .../Backend/FormEditor/StageComponent.js | 101 ++++++++- .../Backend/FormEditor/TreeComponent.js | 35 ++- .../Backend/FormEditor/ViewModel.js | 14 +- typo3/sysext/form/ext_localconf.php | 2 + 26 files changed, 1209 insertions(+), 21 deletions(-) create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-80196-ExtFormSupportMultipleFormElementsPerRow.rst create mode 100644 typo3/sysext/form/Classes/Domain/Model/FormElements/GridContainer.php create mode 100644 typo3/sysext/form/Classes/Domain/Model/FormElements/GridContainerInterface.php create mode 100644 typo3/sysext/form/Classes/Domain/Model/FormElements/GridRow.php create mode 100644 typo3/sysext/form/Classes/Domain/Model/FormElements/GridRowInterface.php create mode 100644 typo3/sysext/form/Classes/ViewHelpers/GridColumnClassAutoConfigurationViewHelper.php create mode 100644 typo3/sysext/form/Resources/Private/Backend/Partials/FormEditor/Inspector/GridColumnViewPortConfigurationEditor.html create mode 100644 typo3/sysext/form/Resources/Private/Frontend/Partials/GridContainer.html create mode 100644 typo3/sysext/form/Resources/Private/Frontend/Partials/GridRow.html create mode 100644 typo3/sysext/form/Resources/Public/Images/gridcontainer.svg create mode 100644 typo3/sysext/form/Resources/Public/Images/gridrow.svg diff --git a/Build/Resources/Public/Less/form.less b/Build/Resources/Public/Less/form.less index 0bc38aa47b1e..60e8d04d9775 100644 --- a/Build/Resources/Public/Less/form.less +++ b/Build/Resources/Public/Less/form.less @@ -749,6 +749,9 @@ textarea { min-height: 100px; } + .container { + width: auto; + } legend.t3-form-form-element-selected { border-color: @module-docheader-border; } @@ -818,6 +821,10 @@ visibility: visible !important; } +.ui-sortable-placeholder.mjs-nestedSortable-error { + outline: 1px dashed #c83c3c !important; +} + // // Icons // diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-80196-ExtFormSupportMultipleFormElementsPerRow.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-80196-ExtFormSupportMultipleFormElementsPerRow.rst new file mode 100644 index 000000000000..69babcb5c68c --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-80196-ExtFormSupportMultipleFormElementsPerRow.rst @@ -0,0 +1,201 @@ +.. include:: ../../Includes.txt + +==================================================================== +Feature: #80196 - EXT:form - support multiple form elements per row +=================================================================== + +See :issue:`80196` + + +Description +=========== + +Two new form element types have been added to the form framework: + +* GridContainer +* GridRow + +Using these 'container' form elements will enable you to define multiple form elements per row. + +Example: + +.. code-block:: typoscript + + type: Form + identifier: example-form-gridcontainer + label: 'Form Grid Container' + prototypeName: standard + renderables: + - + type: Page + identifier: page-1 + label: Page + renderables: + - + type: GridContainer + identifier: gridcontainer-2 + label: 'Grid: Container' + renderables: + - + type: GridRow + identifier: gridrow-2 + label: 'Grid: Row' + renderables: + - + type: SingleSelect + identifier: singleselect-1 + label: 'Single select' + properties: + gridColumnClassAutoConfiguration: + viewPorts: + xs: + numbersOfColumnsToUse: 12 + lg: + numbersOfColumnsToUse: 2 + - + type: Text + identifier: text-1 + label: Text + properties: + gridColumnClassAutoConfiguration: + viewPorts: + xs: + numbersOfColumnsToUse: 6 + lg: + numbersOfColumnsToUse: 5 + - + type: MultiSelect + identifier: multiselect-1 + label: 'Multi select' + properties: + gridColumnClassAutoConfiguration: + viewPorts: + xs: + numbersOfColumnsToUse: 6 + sm: + numbersOfColumnsToUse: 5 + - + type: GridContainer + identifier: gridcontainer-1 + label: 'Grid: Container' + renderables: + - + type: GridRow + identifier: gridrow-1 + label: 'Grid: Row' + renderables: + - + type: Password + identifier: password-1 + label: Password + +Per default, the resulting markup is compatible to Twitter Bootstrap. + +The following options are available now: + +.. code-block:: typoscript + + GridContainer: + ... + properties: + columnClassAutoConfiguration: + gridSize: 12 + viewPorts: + xs: + classPattern: 'col-xs-{@numbersOfColumnsToUse}' + sm: + classPattern: 'col-sm-{@numbersOfColumnsToUse}' + md: + classPattern: 'col-md-{@numbersOfColumnsToUse}' + lg: + classPattern: 'col-lg-{@numbersOfColumnsToUse}' + +and + +.. code-block:: typoscript + + <formElementIdentifier>: + ... + properties: + gridColumnClassAutoConfiguration: + viewPorts: + xs: + numbersOfColumnsToUse: 12 + ... + lg: + numbersOfColumnsToUse: 2 + + +GridContainer.properties.columnClassAutoConfiguration +----------------------------------------------------- + +The example form definition shown above generates the following HTML markup + +.. code-block:: html + + <div class="container"> + <div class="row"> + <div class="col-xs-12 col-sm-3 col-md-4 col-lg-2"> + ... + </div> + <div class="col-xs-6 col-sm-3 col-md-4 col-lg-5"> + ... + </div> + <div class="col-xs-6 col-sm-5 col-md-4 col-lg-5"> + ... + </div> + </div> + </div> + + +GridContainer.properties.columnClassAutoConfiguration.gridSize +-------------------------------------------------------------- + +Total amount of grid columns (default: 12). + + +GridContainer.properties.columnClassAutoConfiguration.viewPorts.<viewPortName>.classPattern +------------------------------------------------------------------------------------------- + +This pattern will be used to generate the HTML class atrribute values for each viewport. +The wildcard '{@numbersOfColumnsToUse}' will be replaced with the calculated grid column numbers. +At the end, all 'classPattern' items for each viewport will be merged together +and written into the class attribute of each form element (all form elements within a 'GridRow'). + +The calculation depends on the option 'gridSize', the amount of the form elements within the +'GridRow' form element and the optional option 'gridColumnClassAutoConfiguration' from the +form element configurations. + + +<formElementIdentifier>.properties.gridColumnClassAutoConfiguration (otional) +----------------------------------------------------------------------------- + +Each form elements within a 'GridRow' element can define the number of grid columns +to use on a 'per viewport' base. + + +<formElementIdentifier>.properties.gridColumnClassAutoConfiguration.viewPorts.<viewPortName> +-------------------------------------------------------------------------------------------- + +The array keys '<viewPortName>' must match with the array keys '<viewPortName>' +from the configuration 'GridContainer.properties.columnClassAutoConfiguration.viewPorts.<viewPortName>' + + +<formElementIdentifier>.properties.gridColumnClassAutoConfiguration.viewPorts.<viewPortName>.numbersOfColumnsToUse +------------------------------------------------------------------------------------------------------------------ + +The number of grid columns to be used by this element for the viewport '<viewPortName>'. + +This number goes hard to the '{@numbersOfColumnsToUse}' wildcard from the configuration +'GridContainer.properties.columnClassAutoConfiguration.viewPorts.<viewPortName>.classPattern' + +If nothing is set, the {@numbersOfColumnsToUse} will be calculated automatically. + + +Impact +====== + +You are now able to add multiple form elements per row via the API and the form editor. + + +.. index:: Backend, Frontend, ext:form \ No newline at end of file diff --git a/typo3/sysext/form/Classes/Domain/Model/FormElements/GridContainer.php b/typo3/sysext/form/Classes/Domain/Model/FormElements/GridContainer.php new file mode 100644 index 000000000000..84a1a125b92f --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Model/FormElements/GridContainer.php @@ -0,0 +1,90 @@ +<?php +declare(strict_types=1); +namespace TYPO3\CMS\Form\Domain\Model\FormElements; + +/* + * 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\Form\Domain\Exception\TypeDefinitionNotValidException; + +/** + * A GridContainer, being part of a bigger Page + * + * This class contains multiple GridRow elements. + * + * Scope: frontend + * **This class is NOT meant to be sub classed by developers.** + */ +class GridContainer extends Section implements GridContainerInterface +{ + + /** + * Register this element at the parent form, if there is a connection to the parent form. + * + * @return void + * @throws TypeDefinitionNotValidException + * @internal + */ + public function registerInFormIfPossible() + { + foreach ($this->getElementsRecursively() as $renderable) { + if ($renderable instanceof GridContainerInterface) { + throw new TypeDefinitionNotValidException( + sprintf('Grid containers ("%s") within grid containers ("%s") are not allowed.', $renderable->getIdentifier(), $this->getIdentifier()), + 1489412790 + ); + } + } + parent::registerInFormIfPossible(); + } + + /** + * Add a new row element at the end of the grid container + * + * @param FormElementInterface $formElement The form element to add + * @return void + * @api + */ + public function addElement(FormElementInterface $formElement) + { + if (!$formElement instanceof GridRowInterface) { + throw new TypeDefinitionNotValidException( + sprintf('The "implementationClassName" for element "%s" (type "%s") does not implement the GridRowInterface.', $formElement->getIdentifier(), $formElement->getType()), + 1489486301 + ); + } + $this->addRenderable($formElement); + } + + /** + * Create a form element with the given $identifier and attach it to this container. + * + * @param string $identifier Identifier of the new form element + * @param string $typeName type of the new form element + * @return FormElementInterface the newly created grid row + * @throws TypeDefinitionNotValidException + * @api + */ + public function createElement(string $identifier, string $typeName): FormElementInterface + { + $element = parent::createElement($identifier, $typeName); + + if (!$element instanceof GridRowInterface) { + throw new TypeDefinitionNotValidException( + sprintf('The "implementationClassName" for element "%s" (type "%s") does not implement the GridRowInterface.', $identifier, $typeName), + 1489486302 + ); + } + return $element; + } +} diff --git a/typo3/sysext/form/Classes/Domain/Model/FormElements/GridContainerInterface.php b/typo3/sysext/form/Classes/Domain/Model/FormElements/GridContainerInterface.php new file mode 100644 index 000000000000..8dfc02a6bef4 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Model/FormElements/GridContainerInterface.php @@ -0,0 +1,23 @@ +<?php +declare(strict_types=1); +namespace TYPO3\CMS\Form\Domain\Model\FormElements; + +/* + * 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! + */ + +/** + * Scope: frontend + */ +interface GridContainerInterface extends FormElementInterface +{ +} diff --git a/typo3/sysext/form/Classes/Domain/Model/FormElements/GridRow.php b/typo3/sysext/form/Classes/Domain/Model/FormElements/GridRow.php new file mode 100644 index 000000000000..06f03b32fa52 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Model/FormElements/GridRow.php @@ -0,0 +1,103 @@ +<?php +declare(strict_types=1); +namespace TYPO3\CMS\Form\Domain\Model\FormElements; + +/* + * 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\Form\Domain\Exception\TypeDefinitionNotValidException; + +/** + * A grid row, being part of a grid container + * + * This class contains multiple FormElements ({@link FormElementInterface}). + * + * Please see {@link FormDefinition} for an in-depth explanation. + * + * Scope: frontend + * **This class is NOT meant to be sub classed by developers.** + */ +class GridRow extends Section implements GridRowInterface +{ + + /** + * Register this element at the parent form, if there is a connection to the parent form. + * + * @return void + * @throws TypeDefinitionNotValidException + * @internal + */ + public function registerInFormIfPossible() + { + if (!$this->getParentRenderable() instanceof GridContainerInterface) { + throw new TypeDefinitionNotValidException( + sprintf('Grid rows ("%s") only allowed within grid containers.', $this->getIdentifier()), + 1489413805 + ); + } + parent::registerInFormIfPossible(); + } + + /** + * Add a new form element at the end of the grid row + * + * @param FormElementInterface $formElement The form element to add + * @return void + * @throws TypeDefinitionNotValidException if FormElement is already added to a section + * @api + */ + public function addElement(FormElementInterface $formElement) + { + if ($formElement instanceof GridContainerInterface) { + throw new TypeDefinitionNotValidException( + sprintf('Grid containers ("%s") within grid rows ("%s") are not allowed.', $formElement->getIdentifier(), $this->getIdentifier()), + 1489413379 + ); + } elseif ($formElement instanceof GridRowInterface) { + throw new TypeDefinitionNotValidException( + sprintf('Grid rows ("%s") within grid rows ("%s") are not allowed.', $formElement->getIdentifier(), $this->getIdentifier()), + 1489413696 + ); + } + + $this->addRenderable($formElement); + } + + /** + * Create a form element with the given $identifier and attach it to this container. + * + * @param string $identifier Identifier of the new form element + * @param string $typeName type of the new form element + * @return GridRowInterface the newly created frid row + * @throws TypeDefinitionNotValidException + * @api + */ + public function createElement(string $identifier, string $typeName): FormElementInterface + { + $element = parent::createElement($identifier, $typeName); + + if ($element instanceof GridContainerInterface) { + throw new TypeDefinitionNotValidException( + sprintf('Grid containers ("%s") within grid rows ("%s") are not allowed.', $element->getIdentifier(), $this->getIdentifier()), + 1489413538 + ); + } elseif ($element instanceof GridRowInterface) { + throw new TypeDefinitionNotValidException( + sprintf('Grid rows ("%s") within grid rows ("%s") are not allowed.', $element->getIdentifier(), $this->getIdentifier()), + 1489413697 + ); + } + + return $element; + } +} diff --git a/typo3/sysext/form/Classes/Domain/Model/FormElements/GridRowInterface.php b/typo3/sysext/form/Classes/Domain/Model/FormElements/GridRowInterface.php new file mode 100644 index 000000000000..31a307f0dd9b --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Model/FormElements/GridRowInterface.php @@ -0,0 +1,23 @@ +<?php +declare(strict_types=1); +namespace TYPO3\CMS\Form\Domain\Model\FormElements; + +/* + * 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! + */ + +/** + * Scope: frontend + */ +interface GridRowInterface extends FormElementInterface +{ +} diff --git a/typo3/sysext/form/Classes/ViewHelpers/GridColumnClassAutoConfigurationViewHelper.php b/typo3/sysext/form/Classes/ViewHelpers/GridColumnClassAutoConfigurationViewHelper.php new file mode 100644 index 000000000000..e70c5f3bcb1d --- /dev/null +++ b/typo3/sysext/form/Classes/ViewHelpers/GridColumnClassAutoConfigurationViewHelper.php @@ -0,0 +1,128 @@ +<?php +declare(strict_types=1); +namespace TYPO3\CMS\Form\ViewHelpers; + +/* + * 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\Fluid\Core\ViewHelper\AbstractViewHelper; +use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface; +use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; +use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic; + +/** + * Scope: frontend + * @api + */ +class GridColumnClassAutoConfigurationViewHelper extends AbstractViewHelper +{ + use CompileWithRenderStatic; + + /** + * @var bool + */ + protected $escapeOutput = false; + + /** + * Initialize the arguments. + * + * @return void + * @internal + */ + public function initializeArguments() + { + parent::initializeArguments(); + $this->registerArgument('element', RootRenderableInterface::class, 'A RootRenderableInterface instance', true); + } + + /** + * @param array $arguments + * @param \Closure $renderChildrenClosure + * @param RenderingContextInterface $renderingContext + * @return string + * @public + */ + public static function renderStatic( + array $arguments, + \Closure $renderChildrenClosure, + RenderingContextInterface $renderingContext + ) { + $formElement = $arguments['element']; + + $gridRowElement = $formElement->getParentRenderable(); + $gridContainerElement = $gridRowElement->getParentRenderable(); + $gridRowEChildElements = $gridRowElement->getElementsRecursively(); + + $gridContainerViewPortConfiguration = $gridContainerElement->getProperties()['gridColumnClassAutoConfiguration']; + if (empty($gridContainerViewPortConfiguration)) { + return ''; + } + + $gridSize = (int)$gridContainerViewPortConfiguration['gridSize']; + + $columnsToCalculate = []; + $usedColumns = []; + foreach ($gridRowEChildElements as $childElement) { + if (empty($childElement->getProperties()['gridColumnClassAutoConfiguration'])) { + foreach ($gridContainerViewPortConfiguration['viewPorts'] as $viewPortName => $configuration) { + $columnsToCalculate[$viewPortName]['elements']++; + } + } else { + $gridColumnViewPortConfiguration = $childElement->getProperties()['gridColumnClassAutoConfiguration']; + foreach ($gridContainerViewPortConfiguration['viewPorts'] as $viewPortName => $configuration) { + $configuration = $gridColumnViewPortConfiguration['viewPorts'][$viewPortName]; + if ( + isset($configuration['numbersOfColumnsToUse']) + && (int)$configuration['numbersOfColumnsToUse'] > 0 + ) { + $usedColumns[$viewPortName]['sum'] += (int)$configuration['numbersOfColumnsToUse']; + if ($childElement->getIdentifier() === $formElement->getIdentifier()) { + $usedColumns[$viewPortName]['concreteNumbersOfColumnsToUse'] = (int)$configuration['numbersOfColumnsToUse']; + if ($usedColumns[$viewPortName]['concreteNumbersOfColumnsToUse'] > $gridSize) { + $usedColumns[$viewPortName]['concreteNumbersOfColumnsToUse'] = $gridSize; + } + } + } else { + $columnsToCalculate[$viewPortName]['elements']++; + } + } + } + } + + $classes = []; + foreach ($gridContainerViewPortConfiguration['viewPorts'] as $viewPortName => $configuration) { + if (isset($usedColumns[$viewPortName]['concreteNumbersOfColumnsToUse'])) { + $numbersOfColumnsToUse = $usedColumns[$viewPortName]['concreteNumbersOfColumnsToUse']; + } else { + $restColumnsToDivide = $gridSize - $usedColumns[$viewPortName]['sum']; + $restElements = (int)$columnsToCalculate[$viewPortName]['elements']; + + if ($restColumnsToDivide < 1) { + $restColumnsToDivide = 1; + } + if ($restElements < 1) { + $restElements = 1; + } + $numbersOfColumnsToUse = floor($restColumnsToDivide / $restElements); + } + + $classes[] = str_replace( + '{@numbersOfColumnsToUse}', + $numbersOfColumnsToUse, + $configuration['classPattern'] + ); + } + + return implode(' ', $classes); + } +} diff --git a/typo3/sysext/form/Classes/ViewHelpers/RenderAllFormValuesViewHelper.php b/typo3/sysext/form/Classes/ViewHelpers/RenderAllFormValuesViewHelper.php index 248d7738c74e..a11e44fcd77d 100644 --- a/typo3/sysext/form/Classes/ViewHelpers/RenderAllFormValuesViewHelper.php +++ b/typo3/sysext/form/Classes/ViewHelpers/RenderAllFormValuesViewHelper.php @@ -81,7 +81,16 @@ class RenderAllFormValuesViewHelper extends AbstractViewHelper $output = ''; foreach ($elements as $element) { - if (!$element instanceof FormElementInterface || $element->getType() === 'Honeypot') { + $renderingOptions = $element->getRenderingOptions(); + + if ( + !$element instanceof FormElementInterface + || $element->getType() === 'Honeypot' + || ( + isset($renderingOptions['_isCompositeFormElement']) + && $renderingOptions['_isCompositeFormElement'] = true + ) + ) { continue; } $value = $formRuntime[$element->getIdentifier()]; diff --git a/typo3/sysext/form/Configuration/Yaml/BaseSetup.yaml b/typo3/sysext/form/Configuration/Yaml/BaseSetup.yaml index ecf401ebc0ed..a8b4fd30af2f 100644 --- a/typo3/sysext/form/Configuration/Yaml/BaseSetup.yaml +++ b/typo3/sysext/form/Configuration/Yaml/BaseSetup.yaml @@ -35,6 +35,8 @@ TYPO3: controllerAction: perform httpMethod: post httpEnctype: 'multipart/form-data' + _isCompositeFormElement: false + _isTopLevelFormElement: true honeypot: enable: true @@ -48,15 +50,54 @@ TYPO3: __inheritances: 10: 'TYPO3.CMS.Form.mixins.formElementMixins.BaseFormElementMixin' implementationClassName: 'TYPO3\CMS\Form\Domain\Model\FormElements\Page' - + renderingOptions: + _isTopLevelFormElement: true + _isCompositeFormElement: true + SummaryPage: __inheritances: 10: 'TYPO3.CMS.Form.prototypes.standard.formElementsDefinition.Page' + renderingOptions: + _isTopLevelFormElement: true + _isCompositeFormElement: false Fieldset: __inheritances: 10: 'TYPO3.CMS.Form.mixins.formElementMixins.FormElementMixin' implementationClassName: 'TYPO3\CMS\Form\Domain\Model\FormElements\Section' + renderingOptions: + _isCompositeFormElement: true + + GridContainer: + __inheritances: + 10: 'TYPO3.CMS.Form.mixins.formElementMixins.FormElementMixin' + implementationClassName: 'TYPO3\CMS\Form\Domain\Model\FormElements\GridContainer' + renderingOptions: + _isCompositeFormElement: true + _isGridContainerFormElement: true + properties: + elementClassAttribute: 'container' + gridColumnClassAutoConfiguration: + gridSize: 12 + viewPorts: + xs: + classPattern: 'col-xs-{@numbersOfColumnsToUse}' + sm: + classPattern: 'col-sm-{@numbersOfColumnsToUse}' + md: + classPattern: 'col-md-{@numbersOfColumnsToUse}' + lg: + classPattern: 'col-lg-{@numbersOfColumnsToUse}' + + GridRow: + __inheritances: + 10: 'TYPO3.CMS.Form.mixins.formElementMixins.FormElementMixin' + implementationClassName: 'TYPO3\CMS\Form\Domain\Model\FormElements\GridRow' + properties: + elementClassAttribute: 'row' + renderingOptions: + _isCompositeFormElement: true + _isGridRowFormElement: true ### FORM ELEMENTS: INPUT ### Text: @@ -289,6 +330,16 @@ TYPO3: containerClassAttribute: 'input' elementClassAttribute: '' elementErrorClassAttribute: 'error' + #gridColumnClassAutoConfiguration: + # viewPorts: + # xs: + # numbersOfColumnsToUse: '' + # sm: + # numbersOfColumnsToUse: '' + # md: + # numbersOfColumnsToUse: '' + # lg: + # numbersOfColumnsToUse: '' TextMixin: __inheritances: diff --git a/typo3/sysext/form/Configuration/Yaml/FormEditorSetup.yaml b/typo3/sysext/form/Configuration/Yaml/FormEditorSetup.yaml index 9ea97c7f1e12..8716b328bd69 100644 --- a/typo3/sysext/form/Configuration/Yaml/FormEditorSetup.yaml +++ b/typo3/sysext/form/Configuration/Yaml/FormEditorSetup.yaml @@ -58,6 +58,8 @@ TYPO3: FormElement-Page: 'Stage/Page' FormElement-SummaryPage: 'Stage/SummaryPage' FormElement-Fieldset: 'Stage/Fieldset' + FormElement-GridContainer: 'Stage/Fieldset' + FormElement-GridRow: 'Stage/Fieldset' FormElement-Text: 'Stage/SimpleTemplate' FormElement-Password: 'Stage/SimpleTemplate' FormElement-AdvancedPassword: 'Stage/SimpleTemplate' @@ -86,6 +88,7 @@ TYPO3: Inspector-PropertyGridEditor: 'Inspector/PropertyGridEditor' Inspector-SingleSelectEditor: 'Inspector/SingleSelectEditor' Inspector-MultiSelectEditor: 'Inspector/MultiSelectEditor' + Inspector-GridColumnViewPortConfigurationEditor: 'Inspector/GridColumnViewPortConfigurationEditor' Inspector-TextareaEditor: 'Inspector/TextareaEditor' Inspector-RemoveElementEditor: 'Inspector/RemoveElementEditor' Inspector-FinishersEditor: 'Inspector/FinishersEditor' @@ -298,6 +301,32 @@ TYPO3: label: 'formEditor.elements.Fieldset.editor.label.label' 800: null + GridContainer: + formEditor: + label: 'formEditor.elements.GridContainer.label' + group: container + groupSorting: 200 + _isCompositeFormElement: true + _isGridContainerFormElement: true + iconIdentifier: 't3-form-icon-gridcontainer' + editors: + 200: + label: 'formEditor.elements.GridContainer.editor.label.label' + 800: null + + GridRow: + formEditor: + label: 'formEditor.elements.GridRow.label' + group: container + groupSorting: 300 + _isCompositeFormElement: true + _isGridRowFormElement: true + iconIdentifier: 't3-form-icon-gridrow' + editors: + 200: + label: 'formEditor.elements.GridRow.editor.label.label' + 800: null + ### FORM ELEMENTS: PAGE TYPES ### Page: formEditor: @@ -815,6 +844,30 @@ TYPO3: editors: 200: label: 'formEditor.elements.FormElement.editor.label.label' + + 700: + identifier: 'gridColumnViewPortConfiguration' + templateName: 'Inspector-GridColumnViewPortConfigurationEditor' + label: 'formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.label' + configurationOptions: + viewPorts: + 10: + viewPortIdentifier: 'xs' + label: 'formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.xs.label' + 20: + viewPortIdentifier: 'sm' + label: 'formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.sm.label' + 30: + viewPortIdentifier: 'md' + label: 'formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.md.label' + 40: + viewPortIdentifier: 'lg' + label: 'formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.lg.label' + numbersOfColumnsToUse: + label: 'formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.numbersOfColumnsToUse.label' + propertyPath: 'properties.gridColumnClassAutoConfiguration.viewPorts.{@viewPortIdentifier}.numbersOfColumnsToUse' + fieldExplanationText: 'formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.numbersOfColumnsToUse.fieldExplanationText' + 800: identifier: 'requiredValidator' templateName: 'Inspector-RequiredValidatorEditor' diff --git a/typo3/sysext/form/Resources/Private/Backend/Partials/FormEditor/Inspector/GridColumnViewPortConfigurationEditor.html b/typo3/sysext/form/Resources/Private/Backend/Partials/FormEditor/Inspector/GridColumnViewPortConfigurationEditor.html new file mode 100644 index 000000000000..a7325c9b34c7 --- /dev/null +++ b/typo3/sysext/form/Resources/Private/Backend/Partials/FormEditor/Inspector/GridColumnViewPortConfigurationEditor.html @@ -0,0 +1,17 @@ +<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" xmlns:formvh="http://typo3.org/ns/TYPO3/CMS/Form/ViewHelpers" data-namespace-typo3-fluid="true"> +<div class="form-editor"> + <div class="t3-form-control-group form-group" data-identifier="editorWrapper"> + <label><span data-template-property="label" /></label><br> + <div class="t3-form-controls btn-group" data-identifier="inspectorEditorControlsWrapper"> + <button type="button" class="btn btn-default" data-identifier="viewportButton"></button> + </div> + </div> + <div class="t3-form-control-group form-group" data-template-property="numbersOfColumnsToUse"> + <label><span data-template-property="numbersOfColumnsToUse-label" /></label> + <div class="t3-form-controls" data-identifier="numbersOfColumnsToUse-inspectorEditorControlsWrapper"> + <input type="number" value="" data-template-property="numbersOfColumnsToUse-propertyPath" class="form-control"> + </div> + <span data-template-property="numbersOfColumnsToUse-fieldExplanationText" /> + </div> +</div> +</html> diff --git a/typo3/sysext/form/Resources/Private/Frontend/Partials/GridContainer.html b/typo3/sysext/form/Resources/Private/Frontend/Partials/GridContainer.html new file mode 100644 index 000000000000..35fdcad33df4 --- /dev/null +++ b/typo3/sysext/form/Resources/Private/Frontend/Partials/GridContainer.html @@ -0,0 +1,9 @@ +<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" xmlns:formvh="http://typo3.org/ns/TYPO3/CMS/Form/ViewHelpers" data-namespace-typo3-fluid="true"> +<formvh:renderRenderable renderable="{element}"> + <div class="{f:if(condition: element.properties.elementClassAttribute, then: '{element.properties.elementClassAttribute}')}"> + <f:for each="{element.elements}" as="element"> + <f:render partial="{element.templateName}" arguments="{element: element}" /> + </f:for> + </div> +</formvh:renderRenderable> +</html> diff --git a/typo3/sysext/form/Resources/Private/Frontend/Partials/GridRow.html b/typo3/sysext/form/Resources/Private/Frontend/Partials/GridRow.html new file mode 100644 index 000000000000..4770cbc90de0 --- /dev/null +++ b/typo3/sysext/form/Resources/Private/Frontend/Partials/GridRow.html @@ -0,0 +1,11 @@ +<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" xmlns:formvh="http://typo3.org/ns/TYPO3/CMS/Form/ViewHelpers" data-namespace-typo3-fluid="true"> +<formvh:renderRenderable renderable="{element}"> + <div class="{f:if(condition: element.properties.elementClassAttribute, then: '{element.properties.elementClassAttribute}')}"> + <f:for each="{element.elements}" as="element"> + <div class="{formvh:gridColumnClassAutoConfiguration(element: element)}"> + <f:render partial="{element.templateName}" arguments="{element: element}" /> + </div> + </f:for> + </div> +</formvh:renderRenderable> +</html> diff --git a/typo3/sysext/form/Resources/Private/Language/Database.xlf b/typo3/sysext/form/Resources/Private/Language/Database.xlf index 24e14571b356..6b5cbb74bbd3 100644 --- a/typo3/sysext/form/Resources/Private/Language/Database.xlf +++ b/typo3/sysext/form/Resources/Private/Language/Database.xlf @@ -565,6 +565,28 @@ <source>Required field</source> </trans-unit> + <trans-unit id="formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.label" xml:space="preserve"> + <source>Grid viewport configuration</source> + </trans-unit> + <trans-unit id="formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.xs.label" xml:space="preserve"> + <source>Extra small</source> + </trans-unit> + <trans-unit id="formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.sm.label" xml:space="preserve"> + <source>Small</source> + </trans-unit> + <trans-unit id="formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.md.label" xml:space="preserve"> + <source>Medium</source> + </trans-unit> + <trans-unit id="formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.lg.label" xml:space="preserve"> + <source>Large</source> + </trans-unit> + <trans-unit id="formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.numbersOfColumnsToUse.label" xml:space="preserve"> + <source>Numbers of columns for viewport "{@viewPortLabel}"</source> + </trans-unit> + <trans-unit id="formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.numbersOfColumnsToUse.fieldExplanationText" xml:space="preserve"> + <source>Leave empty for auto calculation</source> + </trans-unit> + <trans-unit id="formEditor.elements.Page.label" xml:space="preserve"> <source>Page</source> </trans-unit> @@ -592,6 +614,20 @@ <source>Fieldset name</source> </trans-unit> + <trans-unit id="formEditor.elements.GridContainer.label" xml:space="preserve"> + <source>Grid: Container</source> + </trans-unit> + <trans-unit id="formEditor.elements.GridContainer.editor.label.label" xml:space="preserve"> + <source>Container name (not visible within Frontend)</source> + </trans-unit> + + <trans-unit id="formEditor.elements.GridRow.label" xml:space="preserve"> + <source>Grid: Row</source> + </trans-unit> + <trans-unit id="formEditor.elements.GridRow.editor.label.label" xml:space="preserve"> + <source>Row name (not visible within Frontend)</source> + </trans-unit> + <trans-unit id="formEditor.elements.Text.label" xml:space="preserve"> <source>Text</source> </trans-unit> diff --git a/typo3/sysext/form/Resources/Public/Css/form.css b/typo3/sysext/form/Resources/Public/Css/form.css index f301b965cd9c..13f6800775d7 100644 --- a/typo3/sysext/form/Resources/Public/Css/form.css +++ b/typo3/sysext/form/Resources/Public/Css/form.css @@ -670,6 +670,9 @@ #t3-form-stage-container.t3-form-stage-viewmode-preview textarea { min-height: 100px; } +#t3-form-stage-container.t3-form-stage-viewmode-preview .container { + width: auto; +} #t3-form-stage-container.t3-form-stage-viewmode-preview legend.t3-form-form-element-selected { border-color: #c3c3c3; } @@ -729,9 +732,15 @@ outline-offset: -2px !important; visibility: visible !important; } + +.ui-sortable-placeholder.mjs-nestedSortable-error { + outline: 1px dashed #c83c3c !important; +} + .t3-form-icon { margin-right: 1em; } + .t3-form-validation-child-has-error { color: #c83c3c; } diff --git a/typo3/sysext/form/Resources/Public/Images/gridcontainer.svg b/typo3/sysext/form/Resources/Public/Images/gridcontainer.svg new file mode 100644 index 000000000000..d517ef3b7768 --- /dev/null +++ b/typo3/sysext/form/Resources/Public/Images/gridcontainer.svg @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve"> +<path style="fill:#676767;" d="M15,1H2H1v1v13v1h1h13h1v-1V2V1H15z M15,15H2V2h13V15z"/> +<path style="fill:#FFFFFF;" d="M7,8H4v1h3V8z M2,2v13h13V2H2z M8,14H3v-3h5V14z M8,10H3V7h5V10z M8,6H3V3h5V6z M14,14H9v-3h5V14z + M14,10H9V7h5V10z M14,6H9V3h5V6z M7,12H4v1h3V12z M13,8h-3v1h3V8z M7,4H4v1h3V4z M13,12h-3v1h3V12z M13,4h-3v1h3V4z"/> +<path style="fill:#9A9999;" d="M3,6h5V3H3V6z M4,4h3v1H4V4z M3,10h5V7H3V10z M4,8h3v1H4V8z M3,14h5v-3H3V14z M4,12h3v1H4V12z M9,3v3 + h5V3H9z M13,5h-3V4h3V5z M9,10h5V7H9V10z M10,8h3v1h-3V8z M9,14h5v-3H9V14z M10,12h3v1h-3V12z"/> +</svg> diff --git a/typo3/sysext/form/Resources/Public/Images/gridrow.svg b/typo3/sysext/form/Resources/Public/Images/gridrow.svg new file mode 100644 index 000000000000..de48da0bfeef --- /dev/null +++ b/typo3/sysext/form/Resources/Public/Images/gridrow.svg @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve"> +<path style="fill:#9A9999;" d="M15,1H2H1v1v13v1h1h13h1v-1V2V1H15z M15,15H2V2h13V15z"/> +<path style="fill:#FFFFFF;" d="M2,2v13h13V2H2z M8,6H3V3h5V6z M14,6H9V3h5V6z M7,4H4v1h3V4z M13,4h-3v1h3V4z"/> +<path style="fill:#676767;" d="M3,6h5V3H3V6z M4,4h3v1H4V4z M9,3v3h5V3H9z M13,5h-3V4h3V5z"/> +</svg> diff --git a/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor.js b/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor.js index a92bb0c76cb7..14e42a55d5ca 100644 --- a/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor.js +++ b/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor.js @@ -800,6 +800,18 @@ define(['jquery', ); }; + /** + * @public + * + * @param object formElement + * @return object|null + */ + function findEnclosingGridContainerFormElement(formElement) { + return _getRepository().findEnclosingGridContainerFormElement( + _getRepository().findFormElement(formElement) + ); + }; + /** * @public * @@ -1058,6 +1070,7 @@ define(['jquery', getCurrentlySelectedPage: getCurrentlySelectedPage, getLastTopLevelElementOnCurrentPage: getLastTopLevelElementOnCurrentPage, findEnclosingCompositeFormElementWhichIsNotOnTopLevel: findEnclosingCompositeFormElementWhichIsNotOnTopLevel, + findEnclosingGridContainerFormElement: findEnclosingGridContainerFormElement, isRootFormElementSelected: isRootFormElementSelected, getLastFormElementWithinParentFormElement: getLastFormElementWithinParentFormElement, getNonCompositeNonToplevelFormElements: getNonCompositeNonToplevelFormElements, diff --git a/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/Core.js b/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/Core.js index 742e2cb6e69b..d1824db06565 100644 --- a/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/Core.js +++ b/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/Core.js @@ -1053,7 +1053,7 @@ define(['jquery'], function($) { * @throws 1475364956 */ function addFormElement(formElement, referenceFormElement, registerPropertyValidators, disablePublishersOnSet) { - var enclosingCompositeFormElement, identifier, formElementTypeDefinition, parentFormElementsArray, referenceFormElementElements, referenceFormElementTypeDefinition; + var enclosingCompositeFormElement, identifier, formElementTypeDefinition, parentFormElementsArray, parentFormElementTypeDefinition, referenceFormElementElements, referenceFormElementTypeDefinition; utility().assert('object' === $.type(formElement), 'Invalid parameter "formElement"', 1475436224); utility().assert('object' === $.type(referenceFormElement), 'Invalid parameter "referenceFormElement"', 1475364956); @@ -1066,6 +1066,7 @@ define(['jquery'], function($) { formElementTypeDefinition = repository().getFormEditorDefinition('formElements', formElement.get('type')); referenceFormElementTypeDefinition = repository().getFormEditorDefinition('formElements', referenceFormElement.get('type')); + // formElement != Page / SummaryPage && referenceFormElement == Page / Fieldset / GridContainer / GridRow if (!formElementTypeDefinition['_isTopLevelFormElement'] && referenceFormElementTypeDefinition['_isCompositeFormElement']) { if ('array' !== $.type(referenceFormElement.get('renderables'))) { referenceFormElement.set('renderables', [], disablePublishersOnSet); @@ -1075,14 +1076,20 @@ define(['jquery'], function($) { formElement.set('__identifierPath', referenceFormElement.get('__identifierPath') + '/' + formElement.get('identifier'), disablePublishersOnSet); referenceFormElement.get('renderables').push(formElement); } else { + // referenceFormElement == root form element if (referenceFormElement.get('__identifierPath') === getApplicationStateStack().getCurrentState('formDefinition').get('__identifierPath')) { referenceFormElementElements = referenceFormElement.get('renderables'); + // referenceFormElement = last page referenceFormElement = referenceFormElementElements[referenceFormElementElements.length - 1]; + // if formElement == Page / SummaryPage && referenceFormElement != Page / SummaryPage } else if (formElementTypeDefinition['_isTopLevelFormElement'] && !referenceFormElementTypeDefinition['_isTopLevelFormElement']) { + // referenceFormElement = parent Page referenceFormElement = findEnclosingCompositeFormElementWhichIsOnTopLevel(referenceFormElement); + // formElement == Page / SummaryPage / Fieldset / GridContainer / GridRow } else if (formElementTypeDefinition['_isCompositeFormElement']) { enclosingCompositeFormElement = findEnclosingCompositeFormElementWhichIsNotOnTopLevel(referenceFormElement); if (enclosingCompositeFormElement) { + // referenceFormElement = parent Fieldset / GridContainer / GridRow referenceFormElement = enclosingCompositeFormElement; } } @@ -1214,7 +1221,9 @@ define(['jquery'], function($) { * * Drag a Element on a Section Element (tree) */ if (position === 'inside') { + // formElementToMove == Page / SummaryPage utility().assert(!formElementToMoveTypeDefinition['_isTopLevelFormElement'], 'This move is not allowed', 1476993731); + // referenceFormElement != Page / Fieldset / GridContainer / GridRow utility().assert(referenceFormElementTypeDefinition['_isCompositeFormElement'], 'This move is not allowed', 1476993732); formElementToMove.set('__parentRenderable', referenceFormElement, disablePublishersOnSet); @@ -1257,8 +1266,8 @@ define(['jquery'], function($) { } else { /** * This is true on: - * * Drag a Element before an Element on another page (tree) - * * Drag a Element after an Element on another page (tree) + * * Drag a Element before an Element on another page (tree / stage) + * * Drag a Element after an Element on another page (tree / stage) */ formElementToMove.set('__parentRenderable', referenceFormElement.get('__parentRenderable'), disablePublishersOnSet); reSetIdentifierPath(formElementToMove, referenceFormElement.get('__parentRenderable').get('__identifierPath')); @@ -1321,6 +1330,29 @@ define(['jquery'], function($) { return formElement; }; + /** + * @param object formElement + * @return object|null + * @throws 1489447996 + */ + function findEnclosingGridContainerFormElement(formElement) { + var formElementTypeDefinition; + utility().assert('object' === $.type(formElement), 'Invalid parameter "formElement"', 1489447996); + + formElementTypeDefinition = repository().getFormEditorDefinition('formElements', formElement.get('type')); + while (!formElementTypeDefinition['_isGridContainerFormElement']) { + if (formElementTypeDefinition['_isTopLevelFormElement']) { + return null; + } + formElement = formElement.get('__parentRenderable'); + formElementTypeDefinition = repository().getFormEditorDefinition('formElements', formElement.get('type')); + } + if (formElementTypeDefinition['_isTopLevelFormElement']) { + return null; + } + return formElement; + }; + /** * @param object formElement * @return object|null @@ -1673,6 +1705,7 @@ define(['jquery'], function($) { findFormElementByIdentifierPath: findFormElementByIdentifierPath, findEnclosingCompositeFormElementWhichIsNotOnTopLevel: findEnclosingCompositeFormElementWhichIsNotOnTopLevel, findEnclosingCompositeFormElementWhichIsOnTopLevel: findEnclosingCompositeFormElementWhichIsOnTopLevel, + findEnclosingGridContainerFormElement: findEnclosingGridContainerFormElement, getIndexForEnclosingCompositeFormElementWhichIsOnTopLevelForFormElement: getIndexForEnclosingCompositeFormElementWhichIsOnTopLevelForFormElement, getNonCompositeNonToplevelFormElements: getNonCompositeNonToplevelFormElements, diff --git a/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/InspectorComponent.js b/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/InspectorComponent.js index 7bb4513daaab..30a3176cfbae 100644 --- a/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/InspectorComponent.js +++ b/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/InspectorComponent.js @@ -60,6 +60,7 @@ define(['jquery', domElementDataAttributeValues: { collapse: 'actions-view-table-expand', editorControlsInputGroup: 'inspectorEditorControlsGroup', + editorWrapper: 'editorWrapper', editorControlsWrapper: 'inspectorEditorControlsWrapper', formElementHeaderEditor: 'inspectorFormElementHeaderEditor', formElementSelectorControlsWrapper: 'inspectorEditorFormElementSelectorControlsWrapper', @@ -78,6 +79,7 @@ define(['jquery', 'Inspector-RequiredValidatorEditor': 'Inspector-RequiredValidatorEditor', 'Inspector-SingleSelectEditor': 'Inspector-SingleSelectEditor', 'Inspector-MultiSelectEditor': 'Inspector-MultiSelectEditor', + 'Inspector-GridColumnViewPortConfigurationEditor': 'Inspector-GridColumnViewPortConfigurationEditor', 'Inspector-TextareaEditor': 'Inspector-TextareaEditor', 'Inspector-TextEditor': 'Inspector-TextEditor', 'Inspector-Typo3WinBrowserEditor': 'Inspector-Typo3WinBrowserEditor', @@ -92,7 +94,8 @@ define(['jquery', propertyGridEditorRowItem: 'rowItem', propertyGridEditorSelectValue: 'selectValue', propertyGridEditorSortRow: 'sortRow', - propertyGridEditorValue: 'value' + propertyGridEditorValue: 'value', + viewportButton: 'viewportButton' }, domElementIdNames: { finisherPrefix: 't3-form-inspector-finishers-', @@ -310,6 +313,14 @@ define(['jquery', collectionName ); break; + case 'Inspector-GridColumnViewPortConfigurationEditor': + renderGridColumnViewPortConfigurationEditor( + editorConfiguration, + editorHtml, + collectionElementIdentifier, + collectionName + ); + break; case 'Inspector-PropertyGridEditor': renderPropertyGridEditor( editorConfiguration, @@ -511,6 +522,16 @@ define(['jquery', getCurrentlySelectedFormElement().set(propertyPathPrefix + propertyPath, newPropertyData); }; + /** + * @private + * + * @param object + * @return object + */ + function _getEditorWrapperDomElement(editorDomElement) { + return $(getHelper().getDomElementDataIdentifierSelector('editorWrapper'), $(editorDomElement)); + }; + /** * @private * @@ -1291,6 +1312,153 @@ define(['jquery', }); }; + /** + * @public + * + * @param object editorConfiguration + * @param object editorHtml + * @param string collectionElementIdentifier + * @param string collectionName + * @return void + * @throws 1489528242 + * @throws 1489528243 + * @throws 1489528244 + * @throws 1489528245 + * @throws 1489528246 + * @throws 1489528247 + */ + function renderGridColumnViewPortConfigurationEditor(editorConfiguration, editorHtml, collectionElementIdentifier, collectionName) { + var editorControlsWrapper, initNumbersOfColumnsField, numbersOfColumnsTemplate, selectElement, viewportButtonTemplate; + assert( + 'object' === $.type(editorConfiguration), + 'Invalid parameter "editorConfiguration"', + 1489528242 + ); + assert( + 'object' === $.type(editorHtml), + 'Invalid parameter "editorHtml"', + 1489528243 + ); + assert( + getUtility().isNonEmptyString(editorConfiguration['label']), + 'Invalid configuration "label"', + 1489528244 + ); + assert( + 'array' === $.type(editorConfiguration['configurationOptions']['viewPorts']), + 'Invalid configurationOptions "viewPorts"', + 1489528245 + ); + assert( + !getUtility().isUndefinedOrNull(editorConfiguration['configurationOptions']['numbersOfColumnsToUse']['label']), + 'Invalid configurationOptions "numbersOfColumnsToUse"', + 1489528246 + ); + assert( + !getUtility().isUndefinedOrNull(editorConfiguration['configurationOptions']['numbersOfColumnsToUse']['propertyPath']), + 'Invalid configuration "selectOptions"', + 1489528247 + ); + + if (!getFormElementDefinition(getCurrentlySelectedFormElement().get('__parentRenderable'), '_isGridRowFormElement')) { + editorHtml.remove(); + return; + } + + getHelper() + .getTemplatePropertyDomElement('label', editorHtml) + .append(editorConfiguration['label']); + + + viewportButtonTemplate = $(getHelper() + .getDomElementDataIdentifierSelector('viewportButton'), $(editorHtml)) + .clone(); + + $(getHelper() + .getDomElementDataIdentifierSelector('viewportButton'), $(editorHtml)) + .remove(); + + numbersOfColumnsTemplate = getHelper() + .getTemplatePropertyDomElement('numbersOfColumnsToUse', $(editorHtml)) + .clone(); + + getHelper() + .getTemplatePropertyDomElement('numbersOfColumnsToUse', $(editorHtml)) + .remove(); + + editorControlsWrapper = _getEditorControlsWrapperDomElement(editorHtml); + + initNumbersOfColumnsField = function(element) { + var numbersOfColumnsTemplateClone, propertyPath; + + getHelper().getTemplatePropertyDomElement('numbersOfColumnsToUse', $(editorHtml)) + .off() + .empty() + .remove(); + + numbersOfColumnsTemplateClone = $(numbersOfColumnsTemplate).clone(true, true); + _getEditorWrapperDomElement(editorHtml).after(numbersOfColumnsTemplateClone); + + $('input', numbersOfColumnsTemplateClone).focus(); + + getHelper() + .getTemplatePropertyDomElement('numbersOfColumnsToUse-label', numbersOfColumnsTemplateClone) + .append( + editorConfiguration['configurationOptions']['numbersOfColumnsToUse']['label'] + .replace('{@viewPortLabel}', element.data('viewPortLabel')) + ); + + getHelper() + .getTemplatePropertyDomElement('numbersOfColumnsToUse-fieldExplanationText', numbersOfColumnsTemplateClone) + .append(editorConfiguration['configurationOptions']['numbersOfColumnsToUse']['fieldExplanationText']); + + propertyPath = editorConfiguration['configurationOptions']['numbersOfColumnsToUse']['propertyPath'] + .replace('{@viewPortIdentifier}', element.data('viewPortIdentifier')); + + getHelper() + .getTemplatePropertyDomElement('numbersOfColumnsToUse-propertyPath', numbersOfColumnsTemplateClone) + .val(getCurrentlySelectedFormElement().get(propertyPath)); + + getHelper().getTemplatePropertyDomElement('numbersOfColumnsToUse-propertyPath', numbersOfColumnsTemplateClone).on('keyup paste change', function() { + var that = $(this); + if (!$.isNumeric(that.val())) { + that.val(''); + } else { + getCurrentlySelectedFormElement().set(propertyPath, that.val()); + } + }); + }; + + for (var i = 0, len = editorConfiguration['configurationOptions']['viewPorts'].length; i < len; ++i) { + var numbersOfColumnsTemplateClone, viewportButtonTemplateClone, viewPortIdentifier, viewPortLabel; + + viewPortIdentifier = editorConfiguration['configurationOptions']['viewPorts'][i]['viewPortIdentifier']; + viewPortLabel = editorConfiguration['configurationOptions']['viewPorts'][i]['label']; + + viewportButtonTemplateClone = $(viewportButtonTemplate).clone(true, true); + viewportButtonTemplateClone.text(viewPortLabel); + viewportButtonTemplateClone.data('viewPortIdentifier', viewPortIdentifier); + viewportButtonTemplateClone.data('viewPortLabel', viewPortLabel); + editorControlsWrapper.append(viewportButtonTemplateClone); + + if (i === (len - 1)) { + numbersOfColumnsTemplateClone = $(numbersOfColumnsTemplate).clone(true, true); + _getEditorWrapperDomElement(editorHtml).after(numbersOfColumnsTemplateClone); + initNumbersOfColumnsField(viewportButtonTemplateClone); + viewportButtonTemplateClone.addClass(getHelper().getDomElementClassName('active')); + } + + $('button', editorControlsWrapper).on('click', function() { + var that = $(this); + + $('button', editorControlsWrapper).removeClass(getHelper().getDomElementClassName('active')); + that.addClass(getHelper().getDomElementClassName('active')); + + initNumbersOfColumnsField(that); + }); + } + }; + /** * @public * diff --git a/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/Mediator.js b/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/Mediator.js index 012f49e63cdc..c9c6e6d27329 100644 --- a/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/Mediator.js +++ b/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/Mediator.js @@ -409,6 +409,7 @@ define(['jquery', * @param string * @param array * args[0] = targetEvent + * args[1] = configuration * @return void * @subscribe view/stage/abstract/elementToolbar/button/newElement/clicked */ @@ -416,7 +417,7 @@ define(['jquery', if (getFormEditorApp().isRootFormElementSelected()) { getViewModel().selectPageBatch(0); } - getViewModel().showInsertElementsModal(args[0]); + getViewModel().showInsertElementsModal(args[0], args[1]); }); /** @@ -425,6 +426,7 @@ define(['jquery', * @param string * @param array * args[0] = targetEvent + * args[1] = configuration * @return void * @subscribe view/newElementButton/clicked */ @@ -432,7 +434,7 @@ define(['jquery', if (getFormEditorApp().isRootFormElementSelected()) { getViewModel().selectPageBatch(0); } - getViewModel().showInsertElementsModal(args[0]); + getViewModel().showInsertElementsModal(args[0], args[1]); }); /** @@ -461,6 +463,7 @@ define(['jquery', getPublisherSubscriber().subscribe('view/stage/abstract/dnd/stop', function(topic, args) { getFormEditorApp().setCurrentlySelectedFormElement(args[0]); getViewModel().renewStructure(); + getViewModel().renderAbstractStageArea(false, false); getViewModel().refreshSelectedElementItemsBatch(); getViewModel().addAbstractViewValidationResults(); getViewModel().renderInspectorEditors(); diff --git a/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/ModalsComponent.js b/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/ModalsComponent.js index 7f81a37fd0d6..ce08eb9807c3 100644 --- a/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/ModalsComponent.js +++ b/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/ModalsComponent.js @@ -43,7 +43,8 @@ define(['jquery', buttonWarning: 'btn-warning' }, domElementDataAttributeNames: { - elementType: 'element-type' + elementType: 'element-type', + fullElementType: 'data-element-type' }, domElementDataAttributeValues: { rowItem: 'rowItem', @@ -209,17 +210,62 @@ define(['jquery', * * @param object modalContent * @param string publisherTopicName + * @param object configuration * @return void * @publish mixed * @throws 1478910954 */ - function _insertElementsModalSetup(modalContent, publisherTopicName) { + function _insertElementsModalSetup(modalContent, publisherTopicName, configuration) { + var formElementItems; + assert( getUtility().isNonEmptyString(publisherTopicName), 'Invalid parameter "publisherTopicName"', 1478910954 ); + if ('object' === $.type(configuration)) { + for (var key in configuration) { + if (!configuration.hasOwnProperty(key)) { + continue; + } + if ( + key === 'disableElementTypes' + && 'array' === $.type(configuration[key]) + ) { + for (var i = 0, len = configuration[key].length; i < len; ++i) { + $( + getHelper().getDomElementDataAttribute( + 'fullElementType', + 'bracesWithKeyValue', [configuration[key][i]] + ), + modalContent + ).addClass(getHelper().getDomElementClassName('disabled')); + } + } + + if ( + key === 'onlyEnableElementTypes' + && 'array' === $.type(configuration[key]) + ) { + $( + getHelper().getDomElementDataAttribute( + 'fullElementType', + 'bracesWithKey' + ), + modalContent + ).each(function(i, element) { + for (var i = 0, len = configuration[key].length; i < len; ++i) { + var that = $(this); + if (that.data(getHelper().getDomElementDataAttribute('elementType')) !== configuration[key][i]) { + that.addClass(getHelper().getDomElementClassName('disabled')); + } + } + }); + } + } + } + $('a', modalContent).on("click", function(e) { getPublisherSubscriber().publish(publisherTopicName, [$(this).data(getHelper().getDomElementDataAttribute('elementType'))]); $('a', modalContent).off(); @@ -387,16 +433,16 @@ define(['jquery', * @public * * @param string - * @param string + * @param object * @return void */ - function showInsertElementsModal(publisherTopicName) { + function showInsertElementsModal(publisherTopicName, configuration) { var html, template; template = getHelper().getTemplate('templateInsertElements'); if (template.length > 0) { html = $(template.html()); - _insertElementsModalSetup(html, publisherTopicName); + _insertElementsModalSetup(html, publisherTopicName, configuration); Modal.show( getFormElementDefinition(getRootFormElement(), 'modalInsertElementsDialogTitle'), @@ -410,7 +456,6 @@ define(['jquery', * @public * * @param string - * @param string * @return void */ function showInsertPagesModal(publisherTopicName) { diff --git a/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/StageComponent.js b/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/StageComponent.js index 789f56719c5e..ff2a1174374a 100644 --- a/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/StageComponent.js +++ b/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/StageComponent.js @@ -66,6 +66,8 @@ define(['jquery', 'FormElement-ContentElement': 'FormElement-ContentElement', 'FormElement-DatePicker': 'FormElement-DatePicker', 'FormElement-Fieldset': 'FormElement-Fieldset', + 'FormElement-GridContainer': 'FormElement-GridContainer', + 'FormElement-GridRow': 'FormElement-GridRow', 'FormElement-FileUpload': 'FormElement-FileUpload', 'FormElement-Hidden': 'FormElement-Hidden', 'FormElement-ImageUpload': 'FormElement-ImageUpload', @@ -257,6 +259,8 @@ define(['jquery', renderSimpleTemplateWithValidators(formElement, template); break; case 'Fieldset': + case 'GridContainer': + case 'GridRow': case 'SummaryPage': case 'Page': case 'StaticText': @@ -354,6 +358,42 @@ define(['jquery', tolerance: 'pointer', toleranceElement: '> div', + isAllowed: function (placeholder, placeholderParent, currentItem) { + var formElementIdentifierPath, formElementTypeDefinition, targetFormElementIdentifierPath, targetFormElementTypeDefinition; + + formElementIdentifierPath = getAbstractViewFormElementIdentifierPathWithinDomElement($(currentItem)); + targetFormElementIdentifierPath = getAbstractViewFormElementIdentifierPathWithinDomElement($(placeholderParent)); + if (!targetFormElementIdentifierPath) { + targetFormElementIdentifierPath = getFormEditorApp().getCurrentlySelectedPage(); + } + + formElementTypeDefinition = getFormElementDefinition(formElementIdentifierPath); + targetFormElementTypeDefinition = getFormElementDefinition(targetFormElementIdentifierPath); + + if ( + formElementTypeDefinition['_isGridContainerFormElement'] + && getFormEditorApp().findEnclosingGridContainerFormElement(targetFormElementIdentifierPath) + ) { + return false; + } + + if ( + formElementTypeDefinition['_isGridRowFormElement'] + && !targetFormElementTypeDefinition['_isGridContainerFormElement'] + ) { + return false; + } + + if ( + !formElementTypeDefinition['_isGridContainerFormElement'] + && !formElementTypeDefinition['_isGridRowFormElement'] + && targetFormElementTypeDefinition['_isGridContainerFormElement'] + ) { + return false; + } + + return true; + }, start: function(e, o) { getPublisherSubscriber().publish('view/stage/abstract/dnd/start', [$(o.item), $(o.placeholder)]); }, @@ -656,16 +696,70 @@ define(['jquery', getViewModel().hideComponent($(getHelper().getDomElementDataIdentifierSelector('abstractViewToolbarNewElement'), template)); $(getHelper().getDomElementDataIdentifierSelector('abstractViewToolbarNewElementSplitButtonAfter'), template).on('click', function(e) { - getPublisherSubscriber().publish('view/stage/abstract/elementToolbar/button/newElement/clicked', ['view/insertElements/perform/after']); + var disableElementTypes, onlyEnableElementTypes; + + disableElementTypes = []; + onlyEnableElementTypes = []; + if (formElementTypeDefinition['_isGridRowFormElement']) { + onlyEnableElementTypes = ['GridRow']; + disableElementTypes = []; + } else { + disableElementTypes = ['GridRow']; + } + + getPublisherSubscriber().publish('view/stage/abstract/elementToolbar/button/newElement/clicked', [ + 'view/insertElements/perform/after', + { + disableElementTypes: disableElementTypes, + onlyEnableElementTypes: onlyEnableElementTypes + } + ] + ); }); $(getHelper().getDomElementDataIdentifierSelector('abstractViewToolbarNewElementSplitButtonInside'), template).on('click', function(e) { - getPublisherSubscriber().publish('view/stage/abstract/elementToolbar/button/newElement/clicked', ['view/insertElements/perform/inside']); + var disableElementTypes, onlyEnableElementTypes; + + disableElementTypes = ['GridRow']; + onlyEnableElementTypes = []; + if (formElementTypeDefinition['_isGridContainerFormElement']) { + onlyEnableElementTypes = ['GridRow']; + disableElementTypes = []; + } else if (formElementTypeDefinition['_isGridRowFormElement']) { + onlyEnableElementTypes = []; + disableElementTypes = ['GridContainer', 'GridRow']; + } + + getPublisherSubscriber().publish('view/stage/abstract/elementToolbar/button/newElement/clicked', [ + 'view/insertElements/perform/inside', + { + disableElementTypes: disableElementTypes, + onlyEnableElementTypes: onlyEnableElementTypes + } + ] + ); }); } else { getViewModel().hideComponent($(getHelper().getDomElementDataIdentifierSelector('abstractViewToolbarNewElementSplitButton'), template)); $(getHelper().getDomElementDataIdentifierSelector('abstractViewToolbarNewElement'), template).on('click', function(e) { - getPublisherSubscriber().publish('view/stage/abstract/elementToolbar/button/newElement/clicked', ['view/insertElements/perform/after']); + var disableElementTypes, onlyEnableElementTypes; + + disableElementTypes = []; + onlyEnableElementTypes = []; + if (getFormEditorApp().findEnclosingGridContainerFormElement(formElement)) { + disableElementTypes = ['GridContainer', 'GridRow']; + } else { + disableElementTypes = ['GridRow']; + } + + getPublisherSubscriber().publish( + 'view/stage/abstract/elementToolbar/button/newElement/clicked', [ + 'view/insertElements/perform/after', + { + disableElementTypes: disableElementTypes + } + ] + ); }); } @@ -776,6 +870,7 @@ define(['jquery', if ( !getFormElementDefinition(formElement, '_isTopLevelFormElement') && getFormElementDefinition(formElement, '_isCompositeFormElement') + && !getFormElementDefinition(formElement, '_isGridContainerFormElement') ) { $(this).tooltip({ title: 'identifier: ' + formElement.get('identifier') + ' (type: ' + formElement.get('type') + ')', diff --git a/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/TreeComponent.js b/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/TreeComponent.js index 00d890ff3b5a..f9c0ac090af6 100644 --- a/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/TreeComponent.js +++ b/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/TreeComponent.js @@ -306,7 +306,7 @@ define(['jquery', protectRoot: true, isTree: true, handle: 'div' + getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey'), - helper: 'clone', + helper: 'clone', items: 'li', opacity: .6, revert: 250, @@ -314,6 +314,39 @@ define(['jquery', tolerance: 'pointer', toleranceElement: '> div', + isAllowed: function (placeholder, placeholderParent, currentItem) { + var formElementIdentifierPath, formElementTypeDefinition, targetFormElementIdentifierPath, targetFormElementTypeDefinition; + + formElementIdentifierPath = getTreeNodeIdentifierPathWithinDomElement($(currentItem)); + targetFormElementIdentifierPath = getTreeNodeIdentifierPathWithinDomElement($(placeholderParent)); + + formElementTypeDefinition = getFormElementDefinition(formElementIdentifierPath); + targetFormElementTypeDefinition = getFormElementDefinition(targetFormElementIdentifierPath); + + if ( + formElementTypeDefinition['_isGridContainerFormElement'] + && getFormEditorApp().findEnclosingGridContainerFormElement(targetFormElementIdentifierPath) + ) { + return false; + } + + if ( + formElementTypeDefinition['_isGridRowFormElement'] + && !targetFormElementTypeDefinition['_isGridContainerFormElement'] + ) { + return false; + } + + if ( + !formElementTypeDefinition['_isGridContainerFormElement'] + && !formElementTypeDefinition['_isGridRowFormElement'] + && targetFormElementTypeDefinition['_isGridContainerFormElement'] + ) { + return false; + } + + return true; + }, stop: function(e, o) { getPublisherSubscriber().publish('view/tree/dnd/stop', [getTreeNodeIdentifierPathWithinDomElement($(o.item))]); }, diff --git a/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/ViewModel.js b/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/ViewModel.js index 92741f0ebf08..fe699912bcd7 100644 --- a/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/ViewModel.js +++ b/typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/ViewModel.js @@ -402,7 +402,14 @@ define(['jquery', }); $(getHelper().getDomElementDataIdentifierSelector('buttonStageNewElementBottom')).on('click', function(e) { - getPublisherSubscriber().publish('view/stage/abstract/button/newElement/clicked', ['view/insertElements/perform/bottom']); + getPublisherSubscriber().publish( + 'view/stage/abstract/button/newElement/clicked', [ + 'view/insertElements/perform/bottom', + { + disableElementTypes: ['GridRow'] + } + ] + ); }); $(getHelper().getDomElementDataIdentifierSelector('buttonHeaderNewPage')).on('click', function(e) { @@ -717,10 +724,11 @@ define(['jquery', * @public * * @param string targetEvent + * @param object configuration * @return void */ - function showInsertElementsModal(targetEvent) { - getModals().showInsertElementsModal(targetEvent); + function showInsertElementsModal(targetEvent, configuration) { + getModals().showInsertElementsModal(targetEvent, configuration); }; /** diff --git a/typo3/sysext/form/ext_localconf.php b/typo3/sysext/form/ext_localconf.php index dc6c2519a3f2..9ddd5f1bc234 100644 --- a/typo3/sysext/form/ext_localconf.php +++ b/typo3/sysext/form/ext_localconf.php @@ -27,6 +27,8 @@ call_user_func(function () { 'file-upload', 'finisher', 'form-element-selector', + 'gridcontainer', + 'gridrow', 'hidden', 'image-upload', 'insert-after', -- GitLab