[FEATURE] Show list of failed input fields in FormEngine 52/51452/13
authorAndreas Fernandez <a.fernandez@scripting-base.de>
Sat, 28 Jan 2017 21:04:41 +0000 (22:04 +0100)
committerGeorg Ringer <georg.ringer@gmail.com>
Mon, 6 Feb 2017 20:12:21 +0000 (21:12 +0100)
When validating input fields of the FormEngine fails, a button is now
rendered into the least possible button bar in the module document header.
Clicking the button renders a list of all input elements whose validation
failed.

Clicking onto a field in that list automatically focuses the field in the
form.

Resolves: #79521
Releases: master
Change-Id: I9e232f4d1b27216ccf4a1c7b88d4a9c70b49f4f0
Reviewed-on: https://review.typo3.org/51452
Reviewed-by: Frank Nägler <frank.naegler@typo3.org>
Tested-by: Frank Nägler <frank.naegler@typo3.org>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Build/Resources/Public/Less/TYPO3/_element_popover.less
Build/Resources/Public/Less/TYPO3/_main_form.less
Build/tsconfig.json
typo3/sysext/backend/Classes/Form/FormResultCompiler.php
typo3/sysext/backend/Resources/Private/TypeScript/FormEngineReview.ts [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/Css/backend.css
typo3/sysext/backend/Resources/Public/JavaScript/ColorPicker.js
typo3/sysext/backend/Resources/Public/JavaScript/FormEngineReview.js [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/FormEngineValidation.js
typo3/sysext/core/Documentation/Changelog/master/Feature-79521-ShowListOfFailedInputElementsInFormEngine.rst [new file with mode: 0644]
typo3/sysext/lang/Resources/Private/Language/locallang_alt_doc.xlf

index 685e1cc..886015c 100644 (file)
                padding: 12px 14px;
        }
 
-       &-content p {
-               margin: 0;
+       &-content {
+               p {
+                       margin: 0;
+               }
+
+               .list-group {
+                       margin: -9px -14px;
+
+                       .list-group-item {
+                               border-radius: 0;
+                               border-left: 0;
+                               border-right: 0;
+
+                               &:last-child {
+                                       border-bottom: 0;
+                               }
+                       }
+               }
        }
 
        .close {
index 3e3c37c..f518615 100644 (file)
@@ -446,4 +446,4 @@ textarea {
        &.formengine-textarea {
                resize: none;
        }
-}
+}
\ No newline at end of file
index b49e653..8bf3498 100644 (file)
         "../typo3/sysext/*/Resources/Private/TypeScript/**/*.ts"
     ],
     "files": [
-        "../typo3/sysext/backend/Resources/Private/TypeScript/ColorPicker.ts"
+        "../typo3/sysext/backend/Resources/Private/TypeScript/ColorPicker.ts",
+        "../typo3/sysext/backend/Resources/Private/TypeScript/FormEngineReview.ts"
     ]
-}
+}
\ No newline at end of file
index 2307778..ca98485 100644 (file)
@@ -221,6 +221,7 @@ class FormResultCompiler
                        FormEngineValidation.setUsMode(' . ($GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat'] ? '1' : '0') . ');
                        FormEngineValidation.registerReady();
                }';
+        $this->requireJsModules['TYPO3/CMS/Backend/FormEngineReview'] = null;
 
         foreach ($this->requireJsModules as $moduleName => $callbacks) {
             if (!is_array($callbacks)) {
diff --git a/typo3/sysext/backend/Resources/Private/TypeScript/FormEngineReview.ts b/typo3/sysext/backend/Resources/Private/TypeScript/FormEngineReview.ts
new file mode 100644 (file)
index 0000000..2dfbe36
--- /dev/null
@@ -0,0 +1,166 @@
+/*
+ * 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!
+ */
+
+/// <amd-dependency path="bootstrap">
+
+// todo: once FormEngineValidation is a native TypeScript class, we can use require() instead
+// and drop amd-dependency and declare
+/// <amd-dependency path="TYPO3/CMS/Backend/FormEngineValidation" name="FormEngineValidation">
+declare let FormEngineValidation: any;
+declare let TYPO3: any;
+
+import $ = require('jquery');
+
+/**
+ * Module: TYPO3/CMS/Backend/FormEngineReview
+ * Enables interaction with record fields that need review
+ * @exports TYPO3/CMS/Backend/FormEngineReview
+ */
+class FormEngineReview {
+    /**
+     * Fetches all fields that have a failed validation
+     *
+     * @return {$}
+     */
+    public static findInvalidField(): any {
+        return $(document).find('.tab-content .' + FormEngineValidation.errorClass);
+    }
+
+    /**
+     * Renders an invisible button to toggle the review panel into the least possible toolbar
+     *
+     * @param {Object} context
+     */
+    public static attachButtonToModuleHeader(context: any): void {
+        let $leastButtonBar: any = $('.t3js-module-docheader-bar-buttons').children().last().find('[role="toolbar"]');
+        let $button: any = $('<a />', {
+            'class': 'btn btn-danger btn-sm hidden ' + context.toggleButtonClass,
+            href: '#',
+            title: TYPO3.lang['buttons.reviewFailedValidationFields'],
+        }).append(
+            $('<span />', {'class': 'fa fa-fw fa-info'})
+        );
+
+        $button.popover({
+            container: 'body',
+            html: true,
+            placement: 'bottom',
+        });
+
+        $leastButtonBar.prepend($button);
+    }
+
+    /**
+     * Class for the toggle button
+     */
+    private toggleButtonClass: string;
+
+    /**
+     * Class for field list items
+     */
+    private fieldListItemClass: string;
+
+    /**
+     * Class of FormEngine labels
+     */
+    private labelSelector: string;
+
+    /**
+     * The constructor, set the class properties default values
+     */
+    constructor() {
+        this.toggleButtonClass = 't3js-toggle-review-panel';
+        this.fieldListItemClass = 't3js-field-item';
+        this.labelSelector = '.t3js-formengine-label';
+
+        this.initialize();
+    }
+
+    /**
+     * Initialize the events
+     */
+    public initialize(): void {
+        let me: any = this;
+        let $document: any = $(document);
+
+        $(function(): void {
+            FormEngineReview.attachButtonToModuleHeader(me);
+        });
+        $document.on('click', '.' + this.fieldListItemClass, this.switchToField);
+        $document.on('t3-formengine-postfieldvalidation', this.checkForReviewableField);
+    }
+
+    /**
+     * Checks if fields have failed validation. In such case, the markup is rendered and the toggle button is unlocked.
+     */
+    public checkForReviewableField = (): void => {
+        let me: any = this;
+        let $invalidFields: any = FormEngineReview.findInvalidField();
+        let $toggleButton: any = $('.' + this.toggleButtonClass);
+
+        if ($invalidFields.length > 0) {
+            let $list: any = $('<div />', {'class': 'list-group'});
+
+            $invalidFields.each(function(): void {
+                let $field: any = $(this);
+                let $input: any = $field.find('[data-formengine-validation-rules]');
+                let inputId: any = $input.attr('id');
+
+                if (typeof inputId === 'undefined') {
+                    inputId = $input.parent().children('[id]').first().attr('id');
+                }
+
+                $list.append(
+                    $('<a />', {
+                        href: '#',
+                        'class': 'list-group-item ' + me.fieldListItemClass,
+                        'data-field-id': inputId,
+                    }).text($field.find(me.labelSelector).text())
+                );
+            });
+
+            $toggleButton.removeClass('hidden');
+
+            // Bootstrap has no official API to update the content of a popover w/o destroying it
+            let $popover: any = $toggleButton.data('bs.popover');
+            $popover.options.content = $list.wrapAll('<div>').parent().html();
+            $popover.setContent();
+            $popover.$tip.addClass($popover.options.placement);
+        } else {
+            $toggleButton.addClass('hidden').popover('hide');
+        }
+    };
+
+    /**
+     * Finds the field in the form and focuses it
+     *
+     * @param {Event} e
+     */
+    public switchToField = (e: Event): void => {
+        e.preventDefault();
+
+        let $listItem: any = $(e.currentTarget);
+        let referenceFieldId: string = $listItem.data('fieldId');
+        let $referenceField: any = $('#' + referenceFieldId);
+
+        // Iterate possibly nested tab panels
+        $referenceField.parents('[id][role="tabpanel"]').each(function(): void {
+            $('[aria-controls="' + $(this).attr('id') + '"]').tab('show');
+        });
+
+        $referenceField.focus();
+    };
+}
+
+// Create an instance and return it
+export = new FormEngineReview();
index 2f3e70c..9e43e99 100644 (file)
@@ -11217,6 +11217,17 @@ fieldset[disabled] .table .btn-default.focus {
 .popover-content p {
   margin: 0;
 }
+.popover-content .list-group {
+  margin: -9px -14px;
+}
+.popover-content .list-group .list-group-item {
+  border-radius: 0;
+  border-left: 0;
+  border-right: 0;
+}
+.popover-content .list-group .list-group-item:last-child {
+  border-bottom: 0;
+}
 .popover .close {
   margin-right: 10px;
   margin-top: 10px;
index ec0ef1b..f7abac0 100644 (file)
@@ -31,7 +31,7 @@ define(["require", "exports", 'jquery', "TYPO3/CMS/Core/Contrib/jquery.minicolor
             $(this.selector).minicolors({
                 format: 'hex',
                 position: 'bottom left',
-                theme: 'bootstrap'
+                theme: 'bootstrap',
             });
         };
         return ColorPicker;
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngineReview.js b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngineReview.js
new file mode 100644 (file)
index 0000000..3aa0eaa
--- /dev/null
@@ -0,0 +1,122 @@
+/*
+ * 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!
+ */
+define(["require", "exports", "TYPO3/CMS/Backend/FormEngineValidation", 'jquery', "bootstrap"], function (require, exports, FormEngineValidation, $) {
+    "use strict";
+    /**
+     * Module: TYPO3/CMS/Backend/FormEngineReview
+     * Enables interaction with record fields that need review
+     * @exports TYPO3/CMS/Backend/FormEngineReview
+     */
+    var FormEngineReview = (function () {
+        /**
+         * The constructor, set the class properties default values
+         */
+        function FormEngineReview() {
+            var _this = this;
+            /**
+             * Checks if fields have failed validation. In such case, the markup is rendered and the toggle button is unlocked.
+             */
+            this.checkForReviewableField = function () {
+                var me = _this;
+                var $invalidFields = FormEngineReview.findInvalidField();
+                var $toggleButton = $('.' + _this.toggleButtonClass);
+                if ($invalidFields.length > 0) {
+                    var $list_1 = $('<div />', { 'class': 'list-group' });
+                    $invalidFields.each(function () {
+                        var $field = $(this);
+                        var $input = $field.find('[data-formengine-validation-rules]');
+                        var inputId = $input.attr('id');
+                        if (typeof inputId === 'undefined') {
+                            inputId = $input.parent().children('[id]').first().attr('id');
+                        }
+                        $list_1.append($('<a />', {
+                            href: '#',
+                            'class': 'list-group-item ' + me.fieldListItemClass,
+                            'data-field-id': inputId,
+                        }).text($field.find(me.labelSelector).text()));
+                    });
+                    $toggleButton.removeClass('hidden');
+                    // Bootstrap has no official API to update the content of a popover w/o destroying it
+                    var $popover = $toggleButton.data('bs.popover');
+                    $popover.options.content = $list_1.wrapAll('<div>').parent().html();
+                    $popover.setContent();
+                    $popover.$tip.addClass($popover.options.placement);
+                }
+                else {
+                    $toggleButton.addClass('hidden').popover('hide');
+                }
+            };
+            /**
+             * Finds the field in the form and focuses it
+             *
+             * @param {Event} e
+             */
+            this.switchToField = function (e) {
+                e.preventDefault();
+                var $listItem = $(e.currentTarget);
+                var referenceFieldId = $listItem.data('fieldId');
+                var $referenceField = $('#' + referenceFieldId);
+                // Iterate possibly nested tab panels
+                $referenceField.parents('[id][role="tabpanel"]').each(function () {
+                    $('[aria-controls="' + $(this).attr('id') + '"]').tab('show');
+                });
+                $referenceField.focus();
+            };
+            this.toggleButtonClass = 't3js-toggle-review-panel';
+            this.fieldListItemClass = 't3js-field-item';
+            this.labelSelector = '.t3js-formengine-label';
+            this.initialize();
+        }
+        /**
+         * Fetches all fields that have a failed validation
+         *
+         * @return {$}
+         */
+        FormEngineReview.findInvalidField = function () {
+            return $(document).find('.tab-content .' + FormEngineValidation.errorClass);
+        };
+        /**
+         * Renders an invisible button to toggle the review panel into the least possible toolbar
+         *
+         * @param {Object} context
+         */
+        FormEngineReview.attachButtonToModuleHeader = function (context) {
+            var $leastButtonBar = $('.t3js-module-docheader-bar-buttons').children().last().find('[role="toolbar"]');
+            var $button = $('<a />', {
+                'class': 'btn btn-danger btn-sm hidden ' + context.toggleButtonClass,
+                href: '#',
+                title: TYPO3.lang['buttons.reviewFailedValidationFields'],
+            }).append($('<span />', { 'class': 'fa fa-fw fa-info' }));
+            $button.popover({
+                container: 'body',
+                html: true,
+                placement: 'bottom',
+            });
+            $leastButtonBar.prepend($button);
+        };
+        /**
+         * Initialize the events
+         */
+        FormEngineReview.prototype.initialize = function () {
+            var me = this;
+            var $document = $(document);
+            $(function () {
+                FormEngineReview.attachButtonToModuleHeader(me);
+            });
+            $document.on('click', '.' + this.fieldListItemClass, this.switchToField);
+            $document.on('t3-formengine-postfieldvalidation', this.checkForReviewableField);
+        };
+        return FormEngineReview;
+    }());
+    return new FormEngineReview();
+});
index 0fd8802..ff6497f 100644 (file)
@@ -542,6 +542,7 @@ define(['jquery', 'TYPO3/CMS/Backend/FormEngine', 'moment'], function ($, FormEn
                                }
                        }
                });
+               $(document).trigger('t3-formengine-postfieldvalidation');
        };
 
        /**
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-79521-ShowListOfFailedInputElementsInFormEngine.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-79521-ShowListOfFailedInputElementsInFormEngine.rst
new file mode 100644 (file)
index 0000000..c31483b
--- /dev/null
@@ -0,0 +1,17 @@
+.. include:: ../../Includes.txt
+
+==================================================================
+Feature: #79521 - Show list of failed input elements in FormEngine
+==================================================================
+
+See :issue:`79521`
+
+Description
+===========
+
+When validating input fields of the FormEngine fails, a button is now rendered into the least possible button bar in
+the module document header. Clicking the button renders a list of all input elements whose validation failed.
+
+Clicking onto a field in that list automatically focuses the field in the form.
+
+.. index:: Backend
\ No newline at end of file
index 36423b0..4698db7 100644 (file)
@@ -81,6 +81,9 @@
                        <trans-unit id="buttons.confirm.delete_elements.yes">
                                <source>Yes, delete these elements</source>
                        </trans-unit>
+                       <trans-unit id="buttons.reviewFailedValidationFields">
+                               <source>Review fields with failed validations</source>
+                       </trans-unit>
                </body>
        </file>
 </xliff>