[FEATURE] EXT:form - support multiple form elements per row 37/52037/20
authorRalf Zimmermann <ralf.zimmermann@tritum.de>
Tue, 14 Mar 2017 03:07:25 +0000 (04:07 +0100)
committerSusanne Moog <susanne.moog@typo3.org>
Sat, 25 Mar 2017 16:48:32 +0000 (17:48 +0100)
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>
26 files changed:
Build/Resources/Public/Less/form.less
typo3/sysext/core/Documentation/Changelog/master/Feature-80196-ExtFormSupportMultipleFormElementsPerRow.rst [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Model/FormElements/GridContainer.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Model/FormElements/GridContainerInterface.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Model/FormElements/GridRow.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Model/FormElements/GridRowInterface.php [new file with mode: 0644]
typo3/sysext/form/Classes/ViewHelpers/GridColumnClassAutoConfigurationViewHelper.php [new file with mode: 0644]
typo3/sysext/form/Classes/ViewHelpers/RenderAllFormValuesViewHelper.php
typo3/sysext/form/Configuration/Yaml/BaseSetup.yaml
typo3/sysext/form/Configuration/Yaml/FormEditorSetup.yaml
typo3/sysext/form/Resources/Private/Backend/Partials/FormEditor/Inspector/GridColumnViewPortConfigurationEditor.html [new file with mode: 0644]
typo3/sysext/form/Resources/Private/Frontend/Partials/GridContainer.html [new file with mode: 0644]
typo3/sysext/form/Resources/Private/Frontend/Partials/GridRow.html [new file with mode: 0644]
typo3/sysext/form/Resources/Private/Language/Database.xlf
typo3/sysext/form/Resources/Public/Css/form.css
typo3/sysext/form/Resources/Public/Images/gridcontainer.svg [new file with mode: 0644]
typo3/sysext/form/Resources/Public/Images/gridrow.svg [new file with mode: 0644]
typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor.js
typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/Core.js
typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/InspectorComponent.js
typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/Mediator.js
typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/ModalsComponent.js
typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/StageComponent.js
typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/TreeComponent.js
typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/ViewModel.js
typo3/sysext/form/ext_localconf.php

index 0bc38aa..60e8d04 100644 (file)
     textarea {
       min-height: 100px;
     }
+    .container {
+      width: auto;
+    }
     legend.t3-form-form-element-selected {
       border-color: @module-docheader-border;
     }
   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 (file)
index 0000000..69babcb
--- /dev/null
@@ -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 (file)
index 0000000..84a1a12
--- /dev/null
@@ -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 (file)
index 0000000..8dfc02a
--- /dev/null
@@ -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 (file)
index 0000000..06f03b3
--- /dev/null
@@ -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 (file)
index 0000000..31a307f
--- /dev/null
@@ -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 (file)
index 0000000..e70c5f3
--- /dev/null
@@ -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);
+    }
+}
index 248d773..a11e44f 100644 (file)
@@ -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()];
index ecf401e..a8b4fd3 100644 (file)
@@ -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:
index 9ea97c7..8716b32 100644 (file)
@@ -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 (file)
index 0000000..a7325c9
--- /dev/null
@@ -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 (file)
index 0000000..35fdcad
--- /dev/null
@@ -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 (file)
index 0000000..4770cbc
--- /dev/null
@@ -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>
index 24e1457..6b5cbb7 100644 (file)
                 <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>
                 <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>
index f301b96..13f6800 100644 (file)
 #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;
 }
   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 (file)
index 0000000..d517ef3
--- /dev/null
@@ -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 (file)
index 0000000..de48da0
--- /dev/null
@@ -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>
index a92bb0c..14e42a5 100644 (file)
@@ -803,6 +803,18 @@ define(['jquery',
             /**
              * @public
              *
+             * @param object formElement
+             * @return object|null
+             */
+            function findEnclosingGridContainerFormElement(formElement) {
+                return _getRepository().findEnclosingGridContainerFormElement(
+                    _getRepository().findFormElement(formElement)
+                );
+            };
+
+            /**
+             * @public
+             *
              * @return object
              */
             function getNonCompositeNonToplevelFormElements() {
@@ -1058,6 +1070,7 @@ define(['jquery',
                 getCurrentlySelectedPage: getCurrentlySelectedPage,
                 getLastTopLevelElementOnCurrentPage: getLastTopLevelElementOnCurrentPage,
                 findEnclosingCompositeFormElementWhichIsNotOnTopLevel: findEnclosingCompositeFormElementWhichIsNotOnTopLevel,
+                findEnclosingGridContainerFormElement: findEnclosingGridContainerFormElement,
                 isRootFormElementSelected: isRootFormElementSelected,
                 getLastFormElementWithinParentFormElement: getLastFormElementWithinParentFormElement,
                 getNonCompositeNonToplevelFormElements: getNonCompositeNonToplevelFormElements,
index 742e2cb..d1824db 100644 (file)
@@ -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'));
@@ -1324,6 +1333,29 @@ define(['jquery'], function($) {
             /**
              * @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
              * @throws 1475364965
              */
             function findEnclosingCompositeFormElementWhichIsNotOnTopLevel(formElement) {
@@ -1673,6 +1705,7 @@ define(['jquery'], function($) {
                 findFormElementByIdentifierPath: findFormElementByIdentifierPath,
                 findEnclosingCompositeFormElementWhichIsNotOnTopLevel: findEnclosingCompositeFormElementWhichIsNotOnTopLevel,
                 findEnclosingCompositeFormElementWhichIsOnTopLevel: findEnclosingCompositeFormElementWhichIsOnTopLevel,
+                findEnclosingGridContainerFormElement: findEnclosingGridContainerFormElement,
                 getIndexForEnclosingCompositeFormElementWhichIsOnTopLevelForFormElement: getIndexForEnclosingCompositeFormElementWhichIsOnTopLevelForFormElement,
                 getNonCompositeNonToplevelFormElements: getNonCompositeNonToplevelFormElements,
 
index 7bb4513..30a3176 100644 (file)
@@ -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,
@@ -517,6 +528,16 @@ define(['jquery',
          * @param object
          * @return object
          */
+        function _getEditorWrapperDomElement(editorDomElement) {
+            return $(getHelper().getDomElementDataIdentifierSelector('editorWrapper'), $(editorDomElement));
+        };
+
+        /**
+         * @private
+         *
+         * @param object
+         * @return object
+         */
         function _getEditorControlsWrapperDomElement(editorDomElement) {
             return $(getHelper().getDomElementDataIdentifierSelector('editorControlsWrapper'), $(editorDomElement));
         };
@@ -1299,6 +1320,153 @@ define(['jquery',
          * @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
+         *
+         * @param object editorConfiguration
+         * @param object editorHtml
+         * @param string collectionElementIdentifier
+         * @param string collectionName
+         * @return void
          * @throws 1475419226
          * @throws 1475419227
          * @throws 1475419228
index 012f49e..c9c6e6d 100644 (file)
@@ -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();
index 7f81a37..ce08eb9 100644 (file)
@@ -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) {
index 789f567..ff2a117 100644 (file)
@@ -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') + ')',
index 00d890f..f9c0ac0 100644 (file)
@@ -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))]);
                 },
index 92741f0..fe69991 100644 (file)
@@ -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);
         };
 
         /**
index dc6c251..9ddd5f1 100644 (file)
@@ -27,6 +27,8 @@ call_user_func(function () {
             'file-upload',
             'finisher',
             'form-element-selector',
+            'gridcontainer',
+            'gridrow',
             'hidden',
             'image-upload',
             'insert-after',