[FEATURE] Add HTML5 date form element 22/56322/11
authorRalf Zimmermann <ralf.zimmermann@tritum.de>
Sat, 17 Mar 2018 16:11:05 +0000 (17:11 +0100)
committerFrank Naegler <frank.naegler@typo3.org>
Thu, 19 Apr 2018 18:03:29 +0000 (20:03 +0200)
Add an HTML5 date form element and a date range server side validator to
the form framework.

Resolves: #82511
Releases: master
Change-Id: Iab9432fd1d2dbc68b9440d244cd655f82561d8f1
Reviewed-on: https://review.typo3.org/56322
Reviewed-by: Kay Strobach <typo3@kay-strobach.de>
Tested-by: Kay Strobach <typo3@kay-strobach.de>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Frank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Naegler <frank.naegler@typo3.org>
16 files changed:
typo3/sysext/core/Documentation/Changelog/master/Feature-82511-ExtFormAddHtml5DateElement.rst [new file with mode: 0644]
typo3/sysext/form/Classes/Hooks/FormElementHooks.php [new file with mode: 0644]
typo3/sysext/form/Classes/Hooks/FormElementsOnSubmitHooks.php [deleted file]
typo3/sysext/form/Classes/Mvc/Property/PropertyMappingConfiguration.php
typo3/sysext/form/Classes/Mvc/Validation/DateRangeValidator.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/Frontend/Partials/Date.html [new file with mode: 0644]
typo3/sysext/form/Resources/Private/Language/Database.xlf
typo3/sysext/form/Resources/Private/Language/locallang.xlf
typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/InspectorComponent.js
typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/StageComponent.js
typo3/sysext/form/Resources/Public/JavaScript/Backend/FormEditor/ViewModel.js
typo3/sysext/form/Tests/Unit/Mvc/Validation/DateRangeValidatorTest.php [new file with mode: 0644]
typo3/sysext/form/ext_localconf.php

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-82511-ExtFormAddHtml5DateElement.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-82511-ExtFormAddHtml5DateElement.rst
new file mode 100644 (file)
index 0000000..e57a8de
--- /dev/null
@@ -0,0 +1,105 @@
+.. include:: ../../Includes.txt
+
+======================================================
+Feature: #82511 - EXT:form add HTML5 date form element
+======================================================
+
+See :issue:`82511`
+
+Description
+===========
+
+
+`Date` form element
+-------------------
+
+The form framework contains a new form element called `Date` which is technically an HTML5 'date'
+form element.
+
+The following smippet shows a comprehensive example on how to use the new element within the form
+definition including the new `DateRange` validator:
+
+.. code-block:: yaml
+
+    type: Date
+    identifier: date-1
+    label: Date
+    defaultValue: '2018-03-02'
+    properties:
+      # default if not defined: 'd.m.Y' (http://php.net/manual/de/datetime.createfromformat.php#refsect1-datetime.createfromformat-parameters)
+      displayFormat: 'd.m.Y'
+      fluidAdditionalAttributes:
+        min: '2018-03-01'
+        max: '2018-03-30'
+        step: '1'
+    validators:
+      -
+        identifier: DateRange
+        options:
+          minimum: '2018-03-01'
+          maximum: '2018-03-30'
+
+The properties `defaultValue`, `properties.fluidAdditionalAttributes.min`,
+`properties.fluidAdditionalAttributes.max` and the `DateRange` valiator options `minimum` and
+`maximum` must have the format 'Y-m-d' which represents the RFC 3339 'full-date' format.
+
+Read more: https://www.w3.org/TR/2011/WD-html-markup-20110405/input.date.html
+
+The `DateRange` validator is the server side validation equivalent to the client side validation
+through the `min` and `max` HTML attribute and should always be used in combination.
+If the `DateRange` validator is added to the form element within the form editor, the `min` and
+`max` HTML attributes are added automatically.
+
+The property `properties.displayFormat` defines the display format of the submitted value within the
+summary step, email finishers etc. but **not** for the form element value itself.
+The display format of the form element value depends on the browser settings and can not be defined!
+
+Read more: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#Value
+
+Browsers which do not support the HTML5 date element gracefully degrade to a text input.
+The HTML5 date element always normalizes the value to the format Y-m-d (RFC 3339 'full-date').
+With a text input, by default the browser has no recognition of which format the date should be in.
+A workaroung coukd be to put a pattern attribute on the date input. Even though the date input does
+not use it, the text input fallback will. By default, the HTML attribute
+'pattern="([0-9]{4})-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])"' is rendered on the date form
+element. Note that this basic regular expression does not support leap years and does not check for
+the correct number of days in a month. But as a start, this should be sufficient.
+The same pattern is used by the form editor to validate the properties `defaultValue` and the
+`DateRange` valiator options `minimum` and `maximum`.
+
+Read more: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#Handling_browser_support
+
+
+`DateRange` server side validation
+----------------------------------
+
+A new validator called DateRange is available.
+The input must be a DateTime object.
+This input can be tested against a minimum date and a maximum date.
+The minimum date and the maximum date are strings.
+The minimum date and the maximum date can be configured through the validator options.
+
+.. code-block:: yaml
+
+    validators:
+      -
+        identifier: DateRange
+        options:
+          # The PHP \DateTime object format of the `minimum` and `maximum` option
+          # @see http://php.net/manual/de/datetime.createfromformat.php#refsect1-datetime.createfromformat-parameters
+          # 'Y-m-d' is the default value of this validator and must have this value
+          # if you use this validator in combination with the `Date` form element.
+          # This is because the HTML5 date value is always a RFC 3339 'full-date' format (Y-m-d)
+          # @see https://www.w3.org/TR/2011/WD-html-markup-20110405/input.date.html#input.date.attrs.value
+          format : 'Y-m-d'
+          minimum: '2018-03-01'
+          maximum: '2018-03-30'
+
+
+Impact
+======
+
+It is now possible to add an HTML5 date form element including corresponding HTML attributes and
+validators.
+
+.. index:: Frontend, Backend, ext:form
diff --git a/typo3/sysext/form/Classes/Hooks/FormElementHooks.php b/typo3/sysext/form/Classes/Hooks/FormElementHooks.php
new file mode 100644 (file)
index 0000000..32fdaef
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+namespace TYPO3\CMS\Form\Hooks;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Error\Error;
+use TYPO3\CMS\Extbase\Object\ObjectManager;
+use TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface;
+use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface;
+use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
+
+/**
+ * Scope: frontend
+ * @internal
+ */
+class FormElementHooks
+{
+
+    /**
+     * This hook is invoked by the FormRuntime for each form element
+     * **after** a form page was submitted but **before** values are
+     * property-mapped, validated and pushed within the FormRuntime's `FormState`.
+     *
+     * @param FormRuntime $formRuntime
+     * @param RenderableInterface $renderable
+     * @param mixed $elementValue submitted value of the element *before post processing*
+     * @param array $requestArguments submitted raw request values
+     * @return mixed
+     * @see FormRuntime::mapAndValidate()
+     * @internal
+     */
+    public function afterSubmit(FormRuntime $formRuntime, RenderableInterface $renderable, $elementValue, array $requestArguments = [])
+    {
+        if ($renderable->getType() === 'AdvancedPassword') {
+            if ($elementValue['password'] !== $elementValue['confirmation']) {
+                $processingRule = $renderable->getRootForm()->getProcessingRule($renderable->getIdentifier());
+                $processingRule->getProcessingMessages()->addError(
+                    GeneralUtility::makeInstance(ObjectManager::class)
+                        ->get(Error::class, 'Password doesn\'t match confirmation', 1334768052)
+                );
+            }
+            $elementValue = $elementValue['password'];
+        }
+
+        return $elementValue;
+    }
+
+    /**
+     * This is a hook that is invoked by the rendering system **before**
+     * the corresponding element is rendered.
+     *
+     * @param FormRuntime $formRuntime
+     * @param RootRenderableInterface $renderable
+     */
+    public function beforeRendering(FormRuntime $formRuntime, RootRenderableInterface $renderable)
+    {
+        if ($renderable->getType() === 'Date') {
+            $date = $formRuntime[$renderable->getIdentifier()];
+            if ($date instanceof \DateTime) {
+                // @see https://www.w3.org/TR/2011/WD-html-markup-20110405/input.date.html#input.date.attrs.value
+                // 'Y-m-d' = https://tools.ietf.org/html/rfc3339#section-5.6 -> full-date
+                $formRuntime[$renderable->getIdentifier()] = $date->format('Y-m-d');
+            }
+        }
+    }
+
+    /**
+     * This hook is invoked whenever a form element is created.
+     * Note that this hook will be called **after** all properties from the
+     * prototype configuration are set in the form element but **before**
+     * the properties from the form definition are set in the form element.
+     *
+     * @param RenderableInterface $renderable
+     */
+    public function initializeFormElement(RenderableInterface $renderable)
+    {
+        if ($renderable->getType() === 'Date' || $renderable->getType() === 'DatePicker') {
+            // Set the property mapping type for the `Date` and `DatePicker` element.
+            $renderable->setDataType('DateTime');
+        }
+    }
+}
diff --git a/typo3/sysext/form/Classes/Hooks/FormElementsOnSubmitHooks.php b/typo3/sysext/form/Classes/Hooks/FormElementsOnSubmitHooks.php
deleted file mode 100644 (file)
index bacae18..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-namespace TYPO3\CMS\Form\Hooks;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Extbase\Error\Error;
-use TYPO3\CMS\Extbase\Object\ObjectManager;
-use TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface;
-use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
-
-/**
- * Scope: frontend
- * @internal
- */
-class FormElementsOnSubmitHooks
-{
-
-    /**
-     * This hook is invoked by the FormRuntime whenever values are mapped and validated
-     * (after a form page was submitted)
-     *
-     * @param FormRuntime $formRuntime
-     * @param RenderableInterface $renderable
-     * @param mixed $elementValue submitted value of the element *before post processing*
-     * @param array $requestArguments submitted raw request values
-     * @return mixed
-     * @see FormRuntime::mapAndValidate()
-     * @internal
-     */
-    public function afterSubmit(FormRuntime $formRuntime, RenderableInterface $renderable, $elementValue, array $requestArguments = [])
-    {
-        if ($renderable->getType() === 'AdvancedPassword') {
-            if ($elementValue['password'] !== $elementValue['confirmation']) {
-                $processingRule = $renderable->getRootForm()->getProcessingRule($renderable->getIdentifier());
-                $processingRule->getProcessingMessages()->addError(
-                    GeneralUtility::makeInstance(ObjectManager::class)
-                        ->get(Error::class, 'Password doesn\'t match confirmation', 1334768052)
-                );
-            }
-            $elementValue = $elementValue['password'];
-        }
-
-        return $elementValue;
-    }
-}
index bc4fc0d..2d4110d 100644 (file)
@@ -19,6 +19,7 @@ use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Extbase\Object\ObjectManager;
+use TYPO3\CMS\Extbase\Property\TypeConverter\DateTimeConverter;
 use TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator;
 use TYPO3\CMS\Form\Domain\Model\FormElements\FileUpload;
 use TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface;
@@ -32,25 +33,27 @@ class PropertyMappingConfiguration
 {
 
     /**
-     * Set the property mapping configuration for the file upload element.
-     * * Add the UploadedFileReferenceConverter to convert an uploaded file to an
-     *   FileReference.
-     * * Add the MimeTypeValidator to the UploadedFileReferenceConverter to
-     *   delete non valid filetypes directly.
-     * * Setup the storage:
-     *   If the property "saveToFileMount" exist for this element it will be used.
-     *   If this file mount or the property "saveToFileMount" does not exist
-     *   the folder in which the form definition lies (persistence identifier) will be used.
-     *   If the form is generated programmatically and therefore no
-     *   persistence identifier exist the default storage "1:/user_upload/" will be used.
+     * This hook is called for each form element after the class
+     * TYPO3\CMS\Form\Domain\Factory\ArrayFormFactory has built the entire form.
      *
      * @param RenderableInterface $renderable
      * @internal
-     * @todo: could we find a not so ugly solution for that?
      */
     public function afterBuildingFinished(RenderableInterface $renderable)
     {
         if ($renderable instanceof FileUpload) {
+            // Set the property mapping configuration for the file upload element.
+            // * Add the UploadedFileReferenceConverter to convert an uploaded file to a
+            //   FileReference.
+            // * Add the MimeTypeValidator to the UploadedFileReferenceConverter to
+            //   delete non-valid file types directly.
+            // * Setup the storage:
+            //   If the property "saveToFileMount" exist for this element it will be used.
+            //   If this file mount or the property "saveToFileMount" does not exist
+            //   the folder in which the form definition lies (persistence identifier) will be used.
+            //   If the form is generated programmatically and therefore no
+            //   persistence identifier exist the default storage "1:/user_upload/" will be used.
+
             /** @var \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration $propertyMappingConfiguration */
             $propertyMappingConfiguration = $renderable->getRootForm()->getProcessingRule($renderable->getIdentifier())->getPropertyMappingConfiguration();
 
@@ -93,6 +96,17 @@ class PropertyMappingConfiguration
             }
 
             $propertyMappingConfiguration->setTypeConverterOptions(UploadedFileReferenceConverter::class, $uploadConfiguration);
+            return;
+        }
+
+        if ($renderable->getType() === 'Date') {
+            // Set the property mapping configuration for the `Date` element.
+
+            /** @var \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration $propertyMappingConfiguration */
+            $propertyMappingConfiguration = $renderable->getRootForm()->getProcessingRule($renderable->getIdentifier())->getPropertyMappingConfiguration();
+            // @see https://www.w3.org/TR/2011/WD-html-markup-20110405/input.date.html#input.date.attrs.value
+            // 'Y-m-d' = https://tools.ietf.org/html/rfc3339#section-5.6 -> full-date
+            $propertyMappingConfiguration->setTypeConverterOption(DateTimeConverter::class, DateTimeConverter::CONFIGURATION_DATE_FORMAT, 'Y-m-d');
         }
     }
 
diff --git a/typo3/sysext/form/Classes/Mvc/Validation/DateRangeValidator.php b/typo3/sysext/form/Classes/Mvc/Validation/DateRangeValidator.php
new file mode 100644 (file)
index 0000000..159fc34
--- /dev/null
@@ -0,0 +1,124 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Mvc\Validation;
+
+/*
+ * 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\Extbase\Validation\Validator\AbstractValidator;
+use TYPO3\CMS\Form\Mvc\Validation\Exception\InvalidValidationOptionsException;
+
+/**
+ * Validator for date ranges
+ *
+ * Scope: frontend
+ * @api
+ */
+class DateRangeValidator extends AbstractValidator
+{
+    /**
+     * @var array
+     */
+    protected $supportedOptions = [
+        'minimum' => ['', 'The minimum date formatted as Y-m-d', 'string'],
+        'maximum' => ['', 'The maximum date formatted as Y-m-d', 'string'],
+        'format' => ['Y-m-d', 'The format of the minimum and maximum option', 'string'],
+    ];
+
+    /**
+     * @param \DateTime $value The value that should be validated
+     * @api
+     */
+    public function isValid($value)
+    {
+        $this->validateOptions();
+
+        if (!($value instanceof \DateTime)) {
+            $this->addError(
+                $this->translateErrorMessage(
+                    'validation.error.1521293685',
+                    'form',
+                    [gettype($value)]
+                ),
+                1521293685
+            );
+
+            return;
+        }
+
+        $minimum = $this->options['minimum'];
+        $maximum = $this->options['maximum'];
+        $format = $this->options['format'];
+        $value->modify('midnight');
+
+        if (
+            $minimum instanceof \DateTime
+            && $value < $minimum
+        ) {
+            $this->addError(
+                $this->translateErrorMessage(
+                    'validation.error.1521293686',
+                    'form',
+                    [$minimum->format($format)]
+                ),
+                1521293686,
+                [$minimum->format($format)]
+            );
+        }
+
+        if (
+            $maximum instanceof \DateTime
+            && $value > $maximum
+        ) {
+            $this->addError(
+                $this->translateErrorMessage(
+                    'validation.error.1521293687',
+                    'form',
+                    [$maximum->format($format)]
+                ),
+                1521293687,
+                [$maximum->format($format)]
+            );
+        }
+    }
+
+    /**
+     * Checks if this validator is correctly configured
+     *
+     * @throws InvalidValidationOptionsException if the configured validation options are incorrect
+     */
+    protected function validateOptions()
+    {
+        if (!empty($this->options['minimum'])) {
+            $minimum = \DateTime::createFromFormat($this->options['format'], $this->options['minimum']);
+            if (!($minimum instanceof \DateTime)) {
+                $message = sprintf('The option "minimum" (%s) could not be converted to \DateTime from format "%s".', $this->options['minimum'], $this->options['format']);
+                throw new InvalidValidationOptionsException($message, 1521293813);
+            }
+
+            $minimum->modify('midnight');
+            $this->options['minimum'] = $minimum;
+        }
+
+        if (!empty($this->options['maximum'])) {
+            $maximum = \DateTime::createFromFormat($this->options['format'], $this->options['maximum']);
+            if (!($maximum instanceof \DateTime)) {
+                $message = sprintf('The option "maximum" (%s) could not be converted to \DateTime from format "%s".', $this->options['maximum'], $this->options['format']);
+                throw new InvalidValidationOptionsException($message, 1521293814);
+            }
+
+            $maximum->modify('midnight');
+            $this->options['maximum'] = $maximum;
+        }
+    }
+}
index 9117b0b..1d13478 100644 (file)
@@ -186,14 +186,24 @@ class RenderAllFormValuesViewHelper extends AbstractViewHelper
     {
         $properties = $element->getProperties();
         if ($object instanceof \DateTime) {
-            if (isset($properties['dateFormat'])) {
+            if (
+                $element->getType() === 'DatePicker'
+                && isset($properties['dateFormat'])
+            ) {
                 $dateFormat = $properties['dateFormat'];
                 if (isset($properties['displayTimeSelector']) && $properties['displayTimeSelector'] === true) {
                     $dateFormat .= ' H:i';
                 }
+            } elseif ($element->getType() === 'Date') {
+                if (isset($properties['displayFormat'])) {
+                    $dateFormat = $properties['displayFormat'];
+                } else {
+                    $dateFormat = 'Y-m-d';
+                }
             } else {
                 $dateFormat = \DateTime::W3C;
             }
+
             return $object->format($dateFormat);
         }
 
index bb445df..b9a00ed 100644 (file)
@@ -192,6 +192,19 @@ TYPO3:
                 -
                   identifier: Number
 
+            Date:
+              __inheritances:
+                10: 'TYPO3.CMS.Form.mixins.formElementMixins.TextMixin'
+              properties:
+                # Rules for the summary step, email finishers etc. but
+                # **not** for the form element value itself.
+                # The display format of the form element value depends on the browser settings.
+                displayFormat: 'd.m.Y'
+                fluidAdditionalAttributes:
+                  # https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#Handling_browser_support
+                  # https://tools.ietf.org/html/rfc3339#section-5.6 -> full-date
+                  pattern: '([0-9]{4})-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])'
+
             ### FORM ELEMENTS: SELECT ###
             Checkbox:
               __inheritances:
@@ -227,7 +240,6 @@ TYPO3:
             DatePicker:
               __inheritances:
                 10: 'TYPO3.CMS.Form.mixins.formElementMixins.FormElementMixin'
-              implementationClassName: 'TYPO3\CMS\Form\Domain\Model\FormElements\DatePicker'
               properties:
                 elementClassAttribute: 'small form-control'
                 timeSelectorClassAttribute: 'mini'
@@ -367,6 +379,13 @@ TYPO3:
               #options:
                 #minimum: '0B'
                 #maximum: '10M'
+            DateRange:
+              implementationClassName: 'TYPO3\CMS\Form\Mvc\Validation\DateRangeValidator'
+              options:
+                # https://tools.ietf.org/html/rfc3339#section-5.6 -> full-date
+                format : 'Y-m-d'
+                #minimum: '2018-03-17'
+                #maximum: '2018-03-17'
 
       ########### MIXINS ###########
       mixins:
index 8554fcb..a781f6f 100644 (file)
@@ -78,6 +78,7 @@ TYPO3:
               FormElement-Telephone: 'Stage/SimpleTemplate'
               FormElement-Url: 'Stage/SimpleTemplate'
               FormElement-Number: 'Stage/SimpleTemplate'
+              FormElement-Date: 'Stage/SimpleTemplate'
 
               # modals
               Modal-InsertElements: 'Modals/InsertElements'
@@ -116,6 +117,8 @@ TYPO3:
                 errorMessage: 'formEditor.formElementPropertyValidatorsDefinition.FormElementIdentifierWithinCurlyBraces.label'
               FileSize:
                 errorMessage: 'formEditor.formElementPropertyValidatorsDefinition.FileSize.label'
+              RFC3339FullDate:
+                errorMessage: 'formEditor.formElementPropertyValidatorsDefinition.RFC3339FullDate.label'
 
             formElementGroups:
               input:
@@ -614,6 +617,78 @@ TYPO3:
                           label: 'formEditor.elements.TextMixin.validators.Number.editor.header.label'
                         9999: null
 
+            Date:
+              formEditor:
+                label: 'formEditor.elements.Date.label'
+                group: html5
+                groupSorting: 500
+                iconIdentifier: 'form-date-picker'
+                predefinedDefaults:
+                  properties:
+                    fluidAdditionalAttributes:
+                      min: ''
+                      max: ''
+                      step: 1
+                editors:
+                  400: null
+                  500:
+                    placeholder: 'formEditor.elements.Date.editor.defaultValue.placeholder'
+                    propertyValidators:
+                      10: 'RFC3339FullDateOrEmpty'
+                  550:
+                    identifier: 'step'
+                    templateName: 'Inspector-TextEditor'
+                    label: 'formEditor.elements.Date.editor.step.label'
+                    fieldExplanationText: 'formEditor.elements.Date.editor.step.fieldExplanationText'
+                    propertyPath: 'properties.fluidAdditionalAttributes.step'
+                    propertyValidators:
+                      10: 'Integer'
+                  900:
+                    selectOptions:
+                      20:
+                        value: 'DateRange'
+                        label: 'formEditor.elements.Date.editor.validators.DateRange.label'
+                      30: null
+                      40: null
+                      50: null
+                      60: null
+                      70: null
+                      80: null
+                      90: null
+
+                propertyCollections:
+                  validators:
+                    10:
+                      identifier: 'DateRange'
+                      editors:
+                        100:
+                          label: 'formEditor.elements.DatePicker.validators.DateRange.editor.header.label'
+                        200:
+                          errorCodes:
+                            10: 1521293685
+                            20: 1521293686
+                            30: 1521293687
+                        250:
+                          identifier: 'minimum'
+                          templateName: 'Inspector-TextEditor'
+                          label: 'formEditor.elements.DatePicker.validators.DateRange.editor.minimum'
+                          placeholder: 'formEditor.elements.DatePicker.validators.DateRange.editor.minimum.placeholder'
+                          propertyPath: 'options.minimum'
+                          propertyValidators:
+                            10: 'RFC3339FullDateOrEmpty'
+                          additionalElementPropertyPaths:
+                            10: 'properties.fluidAdditionalAttributes.min'
+                        300:
+                          identifier: 'maximum'
+                          templateName: 'Inspector-TextEditor'
+                          label: 'formEditor.elements.DatePicker.validators.DateRange.editor.maximum'
+                          placeholder: 'formEditor.elements.DatePicker.validators.DateRange.editor.maximum.placeholder'
+                          propertyPath: 'options.maximum'
+                          propertyValidators:
+                            10: 'RFC3339FullDateOrEmpty'
+                          additionalElementPropertyPaths:
+                            10: 'properties.fluidAdditionalAttributes.max'
+
             ### FORM ELEMENTS: SELECT ###
             Checkbox:
               formEditor:
@@ -999,6 +1074,14 @@ TYPO3:
                   options:
                     minimum: '0B'
                     maximum: '10M'
+            DateRange:
+              formEditor:
+                iconIdentifier: 'form-validator'
+                label: 'formEditor.elements.FormElement.validators.DateRange.editor.header.label'
+                predefinedDefaults:
+                  options:
+                    minimum: ''
+                    maximum: ''
 
       ########### MIXINS ###########
       mixins:
diff --git a/typo3/sysext/form/Resources/Private/Frontend/Partials/Date.html b/typo3/sysext/form/Resources/Private/Frontend/Partials/Date.html
new file mode 100644 (file)
index 0000000..126a51a
--- /dev/null
@@ -0,0 +1,14 @@
+<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}">
+       <f:render partial="Field/Field" arguments="{element: element}" contentAs="elementContent">
+               <f:form.textfield
+                               type="date"
+                               property="{element.identifier}"
+                               id="{element.uniqueIdentifier}"
+                               class="{element.properties.elementClassAttribute} form-control"
+                               errorClass="{element.properties.elementErrorClassAttribute}"
+                               additionalAttributes="{formvh:translateElementProperty(element: element, property: 'fluidAdditionalAttributes')}"
+               />
+       </f:render>
+</formvh:renderRenderable>
+</html>
index 25871c6..59490e4 100644 (file)
             <trans-unit id="formEditor.formElementPropertyValidatorsDefinition.FileSize.label" xml:space="preserve">
                 <source>Invalid file size format, valid e.g. "10B|K|M|G"</source>
             </trans-unit>
+            <trans-unit id="formEditor.formElementPropertyValidatorsDefinition.RFC3339FullDate.label" xml:space="preserve">
+                <source>Invalid date format. Valid format: Y-m-d (2018-03-17)</source>
+            </trans-unit>
 
             <trans-unit id="formEditor.formElementGroups.input.label" xml:space="preserve">
                 <source>Basic elements</source>
             <trans-unit id="formEditor.elements.FormElement.editor.requiredValidator.validationErrorMessage.fieldExplanationText" xml:space="preserve">
                 <source>Error message which is shown if the validation does not succeed</source>
             </trans-unit>
+            <trans-unit id="formEditor.elements.FormElement.validators.DateRange.editor.header.label" xml:space="preserve">
+                <source>Date range</source>
+            </trans-unit>
 
             <trans-unit id="formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.label" xml:space="preserve">
                 <source>Grid viewport configuration</source>
                 <source>Number</source>
             </trans-unit>
 
+            <trans-unit id="formEditor.elements.Date.label" xml:space="preserve">
+                <source>Date</source>
+            </trans-unit>
+            <trans-unit id="formEditor.elements.Date.editor.defaultValue.placeholder" xml:space="preserve">
+                <source>A date in format Y-m-d (e.g 2018-03-17)</source>
+            </trans-unit>
+            <trans-unit id="formEditor.elements.Date.editor.step.fieldExplanationText" xml:space="preserve">
+                <source>Specify the number of days between each occurrence of this event.</source>
+            </trans-unit>
+            <trans-unit id="formEditor.elements.Date.editor.step.label" xml:space="preserve">
+                <source>Frequency</source>
+            </trans-unit>
+            <trans-unit id="formEditor.elements.Date.editor.validators.DateRange.label" xml:space="preserve">
+                <source>Date range</source>
+            </trans-unit>
+            <trans-unit id="formEditor.elements.DatePicker.validators.DateRange.editor.header.label" xml:space="preserve">
+                <source>Date range</source>
+            </trans-unit>
+            <trans-unit id="formEditor.elements.DatePicker.validators.DateRange.editor.minimum" xml:space="preserve">
+                <source>Minimum date</source>
+            </trans-unit>
+            <trans-unit id="formEditor.elements.DatePicker.validators.DateRange.editor.maximum" xml:space="preserve">
+                <source>Maximum date</source>
+            </trans-unit>
+            <trans-unit id="formEditor.elements.DatePicker.validators.DateRange.editor.minimum.placeholder" xml:space="preserve">
+                <source>A date in format Y-m-d (e.g 2018-03-17)</source>
+            </trans-unit>
+            <trans-unit id="formEditor.elements.DatePicker.validators.DateRange.editor.maximum.placeholder" xml:space="preserve">
+                <source>A date in format Y-m-d (e.g 2018-03-17)</source>
+            </trans-unit>
+
             <trans-unit id="formEditor.elements.Password.label" xml:space="preserve">
                 <source>Password</source>
             </trans-unit>
             </trans-unit>
 
             <trans-unit id="formEditor.elements.DatePicker.label" xml:space="preserve">
-                <source>Date picker</source>
+                <source>Date picker (jQuery)</source>
             </trans-unit>
             <trans-unit id="formEditor.elements.DatePicker.editor.dateFormat.label" xml:space="preserve">
                 <source>Date format</source>
             </trans-unit>
         </body>
     </file>
-</xliff>
+</xliff>
\ No newline at end of file
index 845d152..ae95eab 100644 (file)
             <trans-unit id="validation.error.1505305753" xml:space="preserve">
                 <source>The file size can not exceed %s in size.</source>
             </trans-unit>
+            <trans-unit id="validation.error.1521293686" xml:space="preserve">
+                <source>Please choose a date that is not earlier than "%s".</source>
+            </trans-unit>
+            <trans-unit id="validation.error.1521293687" xml:space="preserve">
+                <source>Please choose a date that is not later than "%s".</source>
+            </trans-unit>
             <trans-unit id="form_new_wizard_title" xml:space="preserve">
                 <source>Form</source>
             </trans-unit>
index a97a582..e6ba2da 100644 (file)
@@ -1241,6 +1241,12 @@ define(['jquery',
           .remove();
       }
 
+      if (getUtility().isNonEmptyString(editorConfiguration['placeholder'])) {
+        getHelper()
+          .getTemplatePropertyDomElement('propertyPath', editorHtml)
+          .attr('placeholder', editorConfiguration['placeholder']);
+      }
+
       propertyPath = getFormEditorApp().buildPropertyPath(
         editorConfiguration['propertyPath'],
         collectionElementIdentifier,
index 1c35a8e..36f0b90 100644 (file)
@@ -85,6 +85,7 @@ define(['jquery',
         'FormElement-Url': 'FormElement-Url',
         'FormElement-Telephone': 'FormElement-Telephone',
         'FormElement-Number': 'FormElement-Number',
+        'FormElement-Date': 'FormElement-Date',
         formElementIcon: 'elementIcon',
         iconValidator: 'form-validator',
         multiValueContainer: 'multiValueContainer',
@@ -264,6 +265,7 @@ define(['jquery',
         case 'Telephone':
         case 'Number':
         case 'DatePicker':
+        case 'Date':
           renderSimpleTemplateWithValidators(formElement, template);
           break;
         case 'Fieldset':
index 06ccc3a..c7e4e7f 100644 (file)
@@ -275,6 +275,24 @@ define(['jquery',
           return getFormEditorApp().getFormElementPropertyValidatorDefinition('FileSize')['errorMessage'] || 'invalid value';
         }
       });
+
+      getFormEditorApp().addPropertyValidationValidator('RFC3339FullDate', function(formElement, propertyPath) {
+        if (getUtility().isUndefinedOrNull(formElement.get(propertyPath))) {
+          return;
+        }
+        if (!formElement.get(propertyPath).match(/^([0-9]{4})-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])$/i)) {
+          return getFormEditorApp().getFormElementPropertyValidatorDefinition('RFC3339FullDate')['errorMessage'] || 'invalid value';
+        }
+      });
+
+      getFormEditorApp().addPropertyValidationValidator('RFC3339FullDateOrEmpty', function(formElement, propertyPath) {
+        if (getUtility().isUndefinedOrNull(formElement.get(propertyPath))) {
+          return;
+        }
+        if (formElement.get(propertyPath).length > 0 && !formElement.get(propertyPath).match(/^([0-9]{4})-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])$/i)) {
+          return getFormEditorApp().getFormElementPropertyValidatorDefinition('RFC3339FullDate')['errorMessage'] || 'invalid value';
+        }
+      });
     };
 
     /**
diff --git a/typo3/sysext/form/Tests/Unit/Mvc/Validation/DateRangeValidatorTest.php b/typo3/sysext/form/Tests/Unit/Mvc/Validation/DateRangeValidatorTest.php
new file mode 100644 (file)
index 0000000..1e99fe4
--- /dev/null
@@ -0,0 +1,163 @@
+<?php
+namespace TYPO3\CMS\Form\Tests\Unit\Mvc\Validation;
+
+/*
+ * 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\Mvc\Validation\DateRangeValidator;
+use TYPO3\CMS\Form\Mvc\Validation\Exception\InvalidValidationOptionsException;
+
+/**
+ * Test case
+ */
+class DateRangeValidatorTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
+{
+
+    /**
+     * @test
+     */
+    public function validateOptionsThrowsExceptionIfMinimumOptionIsInvalid()
+    {
+        $this->expectException(InvalidValidationOptionsException::class);
+        $this->expectExceptionCode(1521293813);
+
+        $options = ['minimum' => '1972-01', 'maximum' => ''];
+        $validator = $this->getMockBuilder(DateRangeValidator::class)
+            ->setMethods(['translateErrorMessage'])
+            ->setConstructorArgs([$options])
+            ->getMock();
+
+        $validator->validate(true);
+    }
+
+    /**
+     * @test
+     */
+    public function validateOptionsThrowsExceptionIfMaximumOptionIsInvalid()
+    {
+        $this->expectException(InvalidValidationOptionsException::class);
+        $this->expectExceptionCode(1521293814);
+
+        $options = ['minimum' => '', 'maximum' => '1972-01'];
+        $validator = $this->getMockBuilder(DateRangeValidator::class)
+            ->setMethods(['translateErrorMessage'])
+            ->setConstructorArgs([$options])
+            ->getMock();
+
+        $validator->validate(true);
+    }
+
+    /**
+     * @test
+     */
+    public function DateRangeValidatorReturnsTrueIfInputIsNoDateTime()
+    {
+        $options = ['minimum' => '2018-03-17', 'maximum' => '2018-03-17'];
+        $validator = $this->getMockBuilder(DateRangeValidator::class)
+            ->setMethods(['translateErrorMessage'])
+            ->setConstructorArgs([$options])
+            ->getMock();
+
+        $this->assertTrue($validator->validate(true)->hasErrors());
+    }
+
+    /**
+     * @test
+     */
+    public function DateRangeValidatorReturnsTrueIfInputIsLowerThanMinimumOption()
+    {
+        $input = \DateTime::createFromFormat('Y-m-d', '2018-03-17');
+        $options = ['minimum' => '2018-03-18', 'maximum' => ''];
+        $validator = $this->getMockBuilder(DateRangeValidator::class)
+            ->setMethods(['translateErrorMessage'])
+            ->setConstructorArgs([$options])
+            ->getMock();
+
+        $this->assertTrue($validator->validate($input)->hasErrors());
+    }
+
+    /**
+     * @test
+     */
+    public function DateRangeValidatorReturnsFalseIfInputIsEqualsMinimumOption()
+    {
+        $input = \DateTime::createFromFormat('Y-m-d', '2018-03-18');
+        $options = ['minimum' => '2018-03-18', 'maximum' => ''];
+        $validator = $this->getMockBuilder(DateRangeValidator::class)
+            ->setMethods(['translateErrorMessage'])
+            ->setConstructorArgs([$options])
+            ->getMock();
+
+        $this->assertFalse($validator->validate($input)->hasErrors());
+    }
+
+    /**
+     * @test
+     */
+    public function DateRangeValidatorReturnsFalseIfInputIsGreaterThanMinimumOption()
+    {
+        $input = \DateTime::createFromFormat('Y-m-d', '2018-03-19');
+        $options = ['minimum' => '2018-03-18', 'maximum' => ''];
+        $validator = $this->getMockBuilder(DateRangeValidator::class)
+            ->setMethods(['translateErrorMessage'])
+            ->setConstructorArgs([$options])
+            ->getMock();
+
+        $this->assertFalse($validator->validate($input)->hasErrors());
+    }
+
+    /**
+     * @test
+     */
+    public function DateRangeValidatorReturnsFalseIfInputIsLowerThanMaximumOption()
+    {
+        $input = \DateTime::createFromFormat('Y-m-d', '2018-03-17');
+        $options = ['maximum' => '', 'maximum' => '2018-03-18'];
+        $validator = $this->getMockBuilder(DateRangeValidator::class)
+            ->setMethods(['translateErrorMessage'])
+            ->setConstructorArgs([$options])
+            ->getMock();
+
+        $this->assertFalse($validator->validate($input)->hasErrors());
+    }
+
+    /**
+     * @test
+     */
+    public function DateRangeValidatorReturnsFalseIfInputIsEqualsMaximumOption()
+    {
+        $input = \DateTime::createFromFormat('Y-m-d', '2018-03-18');
+        $options = ['maximum' => '', 'maximum' => '2018-03-18'];
+        $validator = $this->getMockBuilder(DateRangeValidator::class)
+            ->setMethods(['translateErrorMessage'])
+            ->setConstructorArgs([$options])
+            ->getMock();
+
+        $this->assertFalse($validator->validate($input)->hasErrors());
+    }
+
+    /**
+     * @test
+     */
+    public function DateRangeValidatorReturnsTrueIfInputIsGreaterThanMaximumOption()
+    {
+        $input = \DateTime::createFromFormat('Y-m-d', '2018-03-19');
+        $options = ['maximum' => '', 'maximum' => '2018-03-18'];
+        $validator = $this->getMockBuilder(DateRangeValidator::class)
+            ->setMethods(['translateErrorMessage'])
+            ->setConstructorArgs([$options])
+            ->getMock();
+
+        $this->assertTrue($validator->validate($input)->hasErrors());
+    }
+}
index a9060a6..fd5de56 100644 (file)
@@ -26,7 +26,13 @@ call_user_func(function () {
     );
 
     $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'][1489772699]
-        = \TYPO3\CMS\Form\Hooks\FormElementsOnSubmitHooks::class;
+        = \TYPO3\CMS\Form\Hooks\FormElementHooks::class;
+
+    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['initializeFormElement'][1489772699]
+        = \TYPO3\CMS\Form\Hooks\FormElementHooks::class;
+
+    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeRendering'][1489772699]
+        = \TYPO3\CMS\Form\Hooks\FormElementHooks::class;
 
     // FE file upload processing
     $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterBuildingFinished'][1489772699]