[TASK] Refactor formengine required handling 02/39302/22
authorFrank Nägler <typo3@naegler.net>
Sun, 3 May 2015 11:48:19 +0000 (13:48 +0200)
committerFrank Nägler <frank.naegler@typo3.org>
Fri, 26 Jun 2015 16:05:44 +0000 (18:05 +0200)
This patch removes the required images and adds CSS based styling
for required fields and minitems/maxitems validation.

Resolves: #67354
Releases: master
Change-Id: I5dbccb1c84035fa21079b09748c767bc4f3d06d0
Reviewed-on: http://review.typo3.org/39302
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Frank Nägler <frank.naegler@typo3.org>
Tested-by: Frank Nägler <frank.naegler@typo3.org>
25 files changed:
typo3/sysext/backend/Classes/Form/AbstractNode.php
typo3/sysext/backend/Classes/Form/Container/FlexFormElementContainer.php
typo3/sysext/backend/Classes/Form/Container/FlexFormSectionContainer.php
typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php
typo3/sysext/backend/Classes/Form/Container/PaletteAndSingleContainer.php
typo3/sysext/backend/Classes/Form/Element/AbstractFormElement.php
typo3/sysext/backend/Classes/Form/Element/GroupElement.php
typo3/sysext/backend/Classes/Form/Element/InputElement.php
typo3/sysext/backend/Classes/Form/Element/SelectMultipleSideBySideElement.php
typo3/sysext/backend/Classes/Form/Element/SelectSingleBoxElement.php
typo3/sysext/backend/Classes/Form/Element/SelectSingleElement.php
typo3/sysext/backend/Classes/Form/Element/SelectTreeElement.php
typo3/sysext/backend/Classes/Form/Element/TextElement.php
typo3/sysext/backend/Classes/Form/FormEngine.php
typo3/sysext/backend/Resources/Private/Templates/DocumentTemplate/Tabs.html
typo3/sysext/backend/Resources/Public/JavaScript/FormEngine.js
typo3/sysext/backend/Resources/Public/JavaScript/FormEngineValidation.js [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/jsfunc.inline.js
typo3/sysext/backend/Resources/Public/JavaScript/jsfunc.tbe_editor.js
typo3/sysext/lang/locallang_core.xlf
typo3/sysext/rtehtmlarea/Classes/Form/Element/RichTextElement.php
typo3/sysext/rtehtmlarea/Classes/RteHtmlAreaBase.php
typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/_element_tab.less
typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/_main_form.less
typo3/sysext/t3skin/Resources/Public/Css/visual/t3skin.css

index 0928db5..649c072 100644 (file)
@@ -56,10 +56,6 @@ abstract class AbstractNode implements NodeInterface {
         */
        protected function initializeResultArray() {
                return array(
-                       'requiredElements' => array(), // name => value
-                       'requiredFields' => array(), // value => name
-                       'requiredAdditional' => array(), // name => array
-                       'requiredNested' => array(),
                        'additionalJavaScriptPost' => array(),
                        'additionalJavaScriptSubmit' => array(),
                        'additionalHiddenFields' => array(),
@@ -84,18 +80,6 @@ abstract class AbstractNode implements NodeInterface {
                if (!empty($childReturn['extJSCODE'])) {
                        $existing['extJSCODE'] .= LF . $childReturn['extJSCODE'];
                }
-               foreach ($childReturn['requiredElements'] as $name => $value) {
-                       $existing['requiredElements'][$name] = $value;
-               }
-               foreach ($childReturn['requiredFields'] as $value => $name) { // Params swapped ?!
-                       $existing['requiredFields'][$value] = $name;
-               }
-               foreach ($childReturn['requiredAdditional'] as $name => $subArray) {
-                       $existing['requiredAdditional'][$name] = $subArray;
-               }
-               foreach ($childReturn['requiredNested'] as $value => $name) {
-                       $existing['requiredNested'][$value] = $name;
-               }
                foreach ($childReturn['additionalJavaScriptPost'] as $value) {
                        $existing['additionalJavaScriptPost'][] = $value;
                }
@@ -143,4 +127,47 @@ abstract class AbstractNode implements NodeInterface {
                return $value;
        }
 
+       /**
+        * Build JSON string for validations rules and return it
+        * as data attribute for HTML elements.
+        *
+        * @param array $config
+        * @return string
+        */
+       protected function getValidationDataAsDataAttribute(array $config) {
+               return sprintf(' data-formengine-validation-rules="%s" ', htmlspecialchars($this->getValidationDataAsJsonString($config)));
+       }
+
+       /**
+        * Build JSON string for validations rules.
+        *
+        * @param array $config
+        * @return string
+        */
+       protected function getValidationDataAsJsonString(array $config) {
+               $validationRules = array();
+               if (!empty($config['maxitems']) || !empty($config['minitems'])) {
+                       $minItems = (isset($config['minitems'])) ? (int)$config['minitems'] : 0;
+                       $maxItems = (isset($config['maxitems'])) ? (int)$config['maxitems'] : 10000;
+                       $type = ($config['type']) ?: 'range';
+                       if ($config['renderMode'] !== 'tree' && $maxItems <= 1 && $minItems > 0) {
+                               $validationRules[] = array(
+                                       'type' => $type,
+                                       'minItems' => 1,
+                                       'maxItems' => 100000
+                               );
+                       } else {
+                               $validationRules[] = array(
+                                       'type' => $type,
+                                       'minItems' => $minItems,
+                                       'maxItems' => $maxItems
+                               );
+                       }
+               }
+               if (!empty($config['required'])) {
+                       $validationRules[] = array('type' => 'required');
+               }
+               return json_encode($validationRules);
+       }
+
 }
index eb08f89..4a5e60c 100644 (file)
@@ -200,7 +200,7 @@ class FlexFormElementContainer extends AbstractContainer {
                                // @todo: Similar to the processing within SingleElementContainer ... use it from there?!
                                $html = array();
                                $html[] = '<div class="form-section">';
-                               $html[] =       '<div class="form-group t3js-formengine-palette-field">';
+                               $html[] =       '<div class="form-group t3js-formengine-palette-field t3js-formengine-validation-marker">';
                                $html[] =               '<label class="t3js-formengine-label">';
                                $html[] =                       $languageIcon;
                                $html[] =                       BackendUtility::wrapInHelp($parameterArray['_cshKey'], $flexFormFieldName, $processedTitle);
index 20dc008..4bf04e3 100644 (file)
@@ -87,7 +87,7 @@ class FlexFormSectionContainer extends AbstractContainer {
 
                // "New container" handling: Creates a "template" of each possible container and stuffs it
                // somewhere into DOM to be handled with JS magic.
-               // Fun part: Handle the fact that requiredElements and such may be set for children
+               // Fun part: Handle the fact that such things may be set for children
                $containerTemplatesHtml = array();
                foreach ($flexFormFieldsArray as $flexFormContainerName => $flexFormFieldDefinition) {
                        $containerTemplateHtml = array();
@@ -190,4 +190,4 @@ class FlexFormSectionContainer extends AbstractContainer {
                return $GLOBALS['LANG'];
        }
 
-}
\ No newline at end of file
+}
index a93c49f..8a6b09e 100644 (file)
@@ -96,11 +96,6 @@ class InlineControlContainer extends AbstractContainer {
                if (!$maxItems) {
                        $maxItems = 100000;
                }
-               $resultArray['requiredElements'][$parameterArray['itemFormElName']] = array(
-                       $minItems,
-                       $maxItems,
-                       'imgName' => $table . '_' . $row['uid'] . '_' . $field
-               );
 
                // Add the current inline job to the structure stack
                $newStructureItem = array(
@@ -201,10 +196,9 @@ class InlineControlContainer extends AbstractContainer {
 
                // Wrap all inline fields of a record with a <div> (like a container)
                $html .= '<div class="form-group" id="' . $nameObject . '">';
-
                // Add the level links before all child records:
                if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'top') {
-                       $html .= '<div class="form-group">' . $levelLinks . $localizationLinks . '</div>';
+                       $html .= '<div class="form-group t3js-formengine-validation-marker">' . $levelLinks . $localizationLinks . '</div>';
                }
                // If it's required to select from possible child records (reusable children), add a selector box
                if ($config['foreign_selector'] && $config['appearance']['showPossibleRecordsSelector'] !== FALSE) {
@@ -264,7 +258,7 @@ class InlineControlContainer extends AbstractContainer {
                        $resultArray['additionalJavaScriptPost'][] = 'inline.createDragAndDropSorting("' . $nameObject . '_records' . '");';
                }
                // Publish the uids of the child records in the given order to the browser
-               $html .= '<input type="hidden" name="' . $nameForm . '" value="' . implode(',', $relationList) . '" class="inlineRecord" />';
+               $html .= '<input type="hidden" name="' . $nameForm . '" value="' . implode(',', $relationList) . '" ' . $this->getValidationDataAsDataAttribute(array('type' => 'inline', 'minitems' => $minItems, 'maxitems' => $maxItems)) . ' class="inlineRecord" />';
                // Close the wrap for all inline fields (container)
                $html .= '</div>';
 
@@ -578,7 +572,7 @@ class InlineControlContainer extends AbstractContainer {
                if (!empty($allowedList)) {
                        $item .= '<div class="help-block">' . $allowedLabel . '<br>' . $allowedList . '</div>';
                }
-               $item = '<div class="form-group">' . $item . '</div>';
+               $item = '<div class="form-group t3js-formengine-validation-marker">' . $item . '</div>';
                return $item;
        }
 
@@ -648,7 +642,7 @@ class InlineControlContainer extends AbstractContainer {
 
                        // Wrap the selector and add a spacer to the bottom
 
-                       $item = '<div class="input-group form-group ' . $this->inlineData['config'][$nameObject]['md5'] . '">' . $item . '</div>';
+                       $item = '<div class="input-group form-group t3js-formengine-validation-marker ' . $this->inlineData['config'][$nameObject]['md5'] . '">' . $item . '</div>';
                }
                return $item;
        }
index 34560cb..bfd40f3 100644 (file)
@@ -361,6 +361,7 @@ class PaletteAndSingleContainer extends AbstractContainer {
 
                $paletteFieldClasses = array(
                        'form-group',
+                       't3js-formengine-validation-marker',
                        't3js-formengine-palette-field',
                );
                foreach ($additionalPaletteClasses as $class) {
index a698e47..5504839 100644 (file)
@@ -560,7 +560,7 @@ abstract class AbstractFormElement extends AbstractNode {
                        $selector = '<select id="' . str_replace('.', '', uniqid('tceforms-multiselect-', TRUE)) . '" '
                                . ($params['noList'] ? 'style="display: none"' : 'size="' . $sSize . '" class="form-control tceforms-multiselect"')
                                . ($isMultiple ? ' multiple="multiple"' : '')
-                               . ' name="' . $fName . '_list" ' . $onFocus . $params['style'] . $disabled . '>' . implode('', $opt)
+                               . ' name="' . $fName . '_list" ' . $this->getValidationDataAsDataAttribute($config) . $onFocus . $params['style'] . $disabled . '>' . implode('', $opt)
                                . '</select>';
                }
                $icons = array(
index 6cf2c87..226a58a 100644 (file)
@@ -64,20 +64,15 @@ class GroupElement extends AbstractFormElement {
                // "Extra" configuration; Returns configuration for the field based on settings found in the "types" fieldlist.
                $specConf = BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras']);
 
-               // Register properties in requiredElements
-               $resultArray['requiredElements'][$parameterArray['itemFormElName']] = array(
-                       $minitems,
-                       $maxitems,
-                       'imgName' => $table . '_' . $row['uid'] . '_' . $fieldName
+               // Register properties in required elements / validation
+               $attributes['data-formengine-validation-rules'] = htmlspecialchars(
+                       $this->getValidationDataAsJsonString(
+                               array(
+                                       'minitems' => $minitems,
+                                       'maxitems' => $maxitems
+                               )
+                       )
                );
-               $tabAndInlineStack = $this->globalOptions['tabAndInlineStack'];
-               if (!empty($tabAndInlineStack) && preg_match('/^(.+\\])\\[(\\w+)\\]$/', $parameterArray['itemFormElName'], $match)) {
-                       array_shift($match);
-                       $resultArray['requiredNested'][$parameterArray['itemFormElName']] = array(
-                               'parts' => $match,
-                               'level' => $tabAndInlineStack,
-                       );
-               }
 
                // If maxitems==1 then automatically replace the current item (in list and file selector)
                if ($maxitems === 1) {
@@ -93,7 +88,7 @@ class GroupElement extends AbstractFormElement {
                                . ', \'RemoveFirstIfFull\', ' . GeneralUtility::quoteJSvalue($maxitems) . '); ' . $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'];
                }
 
-               $html = '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '_mul" value="' . ($config['multiple'] ? 1 : 0) . '"' . $disabled . ' />';
+               $html = '<input type="hidden" class="t3js-group-hidden-field" name="' . $parameterArray['itemFormElName'] . '_mul" value="' . ($config['multiple'] ? 1 : 0) . '"' . $disabled . ' />';
 
                // Acting according to either "file" or "db" type:
                switch ((string)$config['internal_type']) {
index fc1ac68..8186a77 100644 (file)
@@ -134,21 +134,7 @@ class InputElement extends AbstractFormElement {
                foreach ($evalList as $func) {
                        switch ($func) {
                                case 'required':
-                                       $resultArray['requiredFields'][$table . '_' . $row['uid'] . '_' . $fieldName] = $parameterArray['itemFormElName'];
-                                       $tabAndInlineStack = $this->globalOptions['tabAndInlineStack'];
-                                       if (!empty($tabAndInlineStack) && preg_match('/^(.+\\])\\[(\\w+)\\]$/', $parameterArray['itemFormElName'], $match)) {
-                                               array_shift($match);
-                                               $resultArray['requiredNested'][$parameterArray['itemFormElName']] = array(
-                                                       'parts' => $match,
-                                                       'level' => $tabAndInlineStack,
-                                               );
-                                       }
-                                       // Mark this field for date/time disposal:
-                                       if (array_intersect($evalList, array('date', 'datetime', 'time', 'timesec'))) {
-                                               $resultArray['requiredAdditional'][$parameterArray['itemFormElName']] = array(
-                                                       'isPositiveNumber' => TRUE,
-                                               );
-                                       }
+                                       $attributes['data-formengine-validation-rules'] = $this->getValidationDataAsJsonString(array('required' => TRUE));
                                        break;
                                default:
                                        // @todo: This is ugly: The code should find out on it's own whether a eval definition is a
index 32815e9..8ac89e3 100644 (file)
@@ -63,7 +63,7 @@ class SelectMultipleSideBySideElement extends AbstractFormElement {
 
                // Wizards:
                if (!$disabled) {
-                       $altItem = '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($parameterArray['itemFormElValue']) . '" />';
+                       $altItem = '<input type="hidden" class="t3js-select-hidden-field" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($parameterArray['itemFormElValue']) . '" />';
                        $html = $this->renderWizards(array($html, $altItem), $config['wizards'], $table, $row, $field, $parameterArray, $parameterArray['itemFormElName'], $specConf);
                }
                $this->resultArray['html'] = $html;
@@ -98,21 +98,6 @@ class SelectMultipleSideBySideElement extends AbstractFormElement {
                if (!$maxitems) {
                        $maxitems = 100000;
                }
-               $minitems = MathUtility::forceIntegerInRange($config['minitems'], 0);
-               // Register the required number of elements:
-               $this->resultArray['requiredElements'][$parameterArray['itemFormElName']] = array(
-                       $minitems,
-                       $maxitems,
-                       'imgName' => $table . '_' . $row['uid'] . '_' . $field
-               );
-               $tabAndInlineStack = $this->globalOptions['tabAndInlineStack'];
-               if (!empty($tabAndInlineStack) && preg_match('/^(.+\\])\\[(\\w+)\\]$/', $parameterArray['itemFormElName'], $match)) {
-                       array_shift($match);
-                       $this->resultArray['requiredNested'][$parameterArray['itemFormElName']] = array(
-                               'parts' => $match,
-                               'level' => $tabAndInlineStack,
-                       );
-               }
                // Get "removeItems":
                $removeItems = GeneralUtility::trimExplode(',', $parameterArray['fieldTSConfig']['removeItems'], TRUE);
                // Get the array with selected items:
@@ -192,7 +177,7 @@ class SelectMultipleSideBySideElement extends AbstractFormElement {
                                . htmlspecialchars($config['exclusiveKeys']) . '" id="' . $multiSelectId . '" name="' . htmlspecialchars($parameterArray['itemFormElName']) . '_sel" '
                                . ' class="form-control t3js-formengine-select-itemstoselect" '
                                . ($size ? ' size="' . $size . '"' : '') . ' onchange="' . htmlspecialchars($sOnChange) . '"'
-                               . $parameterArray['onFocus'] . $selector_itemListStyle . '>
+                               . $parameterArray['onFocus'] . $this->getValidationDataAsDataAttribute($config) . $selector_itemListStyle . '>
                                        ' . implode('
                                        ', $opt) . '
                                </select>';
index 7edbc9f..410fe7f 100644 (file)
@@ -140,7 +140,7 @@ class SelectSingleBoxElement extends AbstractFormElement {
                $selectBox = '<select id="' . str_replace('.', '', uniqid($cssPrefix, TRUE)) . '" name="' . htmlspecialchars($parameterArray['itemFormElName']) . '[]" '
                        . 'class="form-control ' . $cssPrefix . '"' . ($size ? ' size="' . $size . '" ' : '')
                        . ' multiple="multiple" onchange="' . htmlspecialchars($sOnChange) . '"' . $parameterArray['onFocus']
-                       . ' ' . $selector_itemListStyle . $disabled . '>
+                       . ' ' . $this->getValidationDataAsDataAttribute($config) . $selector_itemListStyle . $disabled . '>
                                                ' . implode('
                                                ', $opt) . '
                                        </select>';
index f44ecd2..f248c06 100644 (file)
@@ -126,18 +126,6 @@ class SelectSingleElement extends AbstractFormElement {
                        $disabled = TRUE;
                        $onlySelectedIconShown = 1;
                }
-               // Register as required if minitems is greater than zero
-               if (($minItems = MathUtility::forceIntegerInRange($config['minitems'], 0)) > 0) {
-                       $this->resultArray['requiredFields'][$table . '_' . $row['uid'] . '_' . $field] = $parameterArray['itemFormElName'];
-                       $tabAndInlineStack = $this->globalOptions['tabAndInlineStack'];
-                       if (!empty($tabAndInlineStack) && preg_match('/^(.+\\])\\[(\\w+)\\]$/', $parameterArray['itemFormElName'], $match)) {
-                               array_shift($match);
-                               $this->resultArray['requiredNested'][$parameterArray['itemFormElName']] = array(
-                                       'parts' => $match,
-                                       'level' => $tabAndInlineStack,
-                               );
-                       }
-               }
 
                // Icon configuration:
                if ($config['suppress_icons'] === 'IF_VALUE_FALSE') {
@@ -254,6 +242,7 @@ class SelectSingleElement extends AbstractFormElement {
                                <select'
                                        . ' id="' . $selectId . '"'
                                        . ' name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"'
+                                       . $this->getValidationDataAsDataAttribute($config)
                                        . ' class="form-control form-control-adapt"'
                                        . ($size ? ' size="' . $size . '"' : '')
                                        . ' onchange="' . htmlspecialchars($sOnChange) . '"'
index b872f6d..cd2da26 100644 (file)
@@ -58,26 +58,8 @@ class SelectTreeElement extends AbstractFormElement {
                $specConf = BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras']);
                $selItems = FormEngineUtility::getSelectItems($table, $field, $row, $parameterArray);
 
-               $maxitems = (int)$config['maxitems'];
-
                $html = $this->renderField($table, $field, $row, $parameterArray, $config, $selItems);
 
-               // Register the required number of elements
-               $minitems = MathUtility::forceIntegerInRange($config['minitems'], 0);
-               $resultArray['requiredElements'][$parameterArray['itemFormElName']] = array(
-                       $minitems,
-                       $maxitems,
-                       'imgName' => $table . '_' . $row['uid'] . '_' . $field
-               );
-               $tabAndInlineStack = $this->globalOptions['tabAndInlineStack'];
-               if (!empty($tabAndInlineStack) && preg_match('/^(.+\\])\\[(\\w+)\\]$/', $parameterArray['itemFormElName'], $match)) {
-                       array_shift($match);
-                       $resultArray['requiredNested'][$parameterArray['itemFormElName']] = array(
-                               'parts' => $match,
-                               'level' => $tabAndInlineStack,
-                       );
-               }
-
                // Wizards:
                if (!$disabled) {
                        $altItem = '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($parameterArray['itemFormElValue']) . '" />';
index 47d882a..8c03ead 100644 (file)
@@ -90,17 +90,6 @@ class TextElement extends AbstractFormElement {
                }
 
                $evalList = GeneralUtility::trimExplode(',', $config['eval'], TRUE);
-               if (in_array('required', $evalList, TRUE)) {
-                       $resultArray['requiredFields'][$table . '_' . $row['uid'] . '_' . $fieldName] = $parameterArray['itemFormElName'];
-                       $tabAndInlineStack = $this->globalOptions['tabAndInlineStack'];
-                       if (!empty($tabAndInlineStack) && preg_match('/^(.+\\])\\[(\\w+)\\]$/', $parameterArray['itemFormElName'], $match)) {
-                               array_shift($match);
-                               $resultArray['requiredNested'][$parameterArray['itemFormElName']] = array(
-                                       'parts' => $match,
-                                       'level' => $tabAndInlineStack,
-                               );
-                       }
-               }
                // "Extra" configuration; Returns configuration for the field based on settings found in the "types" fieldlist. Traditionally, this is where RTE configuration has been found.
                $specialConfiguration = BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras']);
                // Setting up the altItem form field, which is a hidden field containing the value
@@ -111,10 +100,11 @@ class TextElement extends AbstractFormElement {
                if ($specialConfiguration['rte_only']) {
                        $html = '<p><em>' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.noRTEfound')) . '</em></p>';
                } else {
+                       $attributes = array();
                        // validation
                        foreach ($evalList as $func) {
                                if ($func === 'required') {
-                                       $resultArray['requiredFields'][$table . '_' . $row['uid'] . '_' . $fieldName] = $parameterArray['itemFormElName'];
+                                       $attributes['data-formengine-validation-rules'] = $this->getValidationDataAsJsonString(array('required' => TRUE));
                                } else {
                                        // @todo: This is ugly: The code should find out on it's own whether a eval definition is a
                                        // @todo: keyword like "date", or a class reference. The global registration could be dropped then
@@ -154,7 +144,6 @@ class TextElement extends AbstractFormElement {
                        }
 
                        // calculate attributes
-                       $attributes = array();
                        $attributes['id'] = str_replace('.', '', uniqid('formengine-textarea-', TRUE));
                        $attributes['name'] = $parameterArray['itemFormElName'];
                        if (!empty($styles)) {
index bef2e6e..7a806ef 100644 (file)
@@ -146,38 +146,6 @@ class FormEngine {
         */
        public $hiddenFieldListArr = array();
 
-       /**
-        * Used to register input-field names, which are required. (Done during rendering of the fields).
-        * This information is then used later when the JavaScript is made.
-        *
-        * @var array
-        */
-       protected $requiredFields = array();
-
-       /**
-        * Used to register input-field names, which are required an have additional requirements.
-        * (e.g. like a date/time must be positive integer)
-        * The information of this array is merged with $this->requiredFields later.
-        *
-        * @var array
-        */
-       protected $requiredAdditional = array();
-
-       /**
-        * Used to register the min and max number of elements
-        * for selector boxes where that apply (in the "group" type for instance)
-        *
-        * @var array
-        */
-       protected $requiredElements = array();
-
-       /**
-        * Used to determine where $requiredFields or $requiredElements are nested (in Tabs or IRRE)
-        *
-        * @var array
-        */
-       public $requiredNested = array();
-
        // Internal, registers for user defined functions etc.
        /**
         * Additional HTML code, printed before the form
@@ -332,10 +300,6 @@ class FormEngine {
                $resultArray = $this->nodeFactory->create($options)->render();
                $html = $resultArray['html'];
 
-               $this->requiredElements = $resultArray['requiredElements'];
-               $this->requiredFields = $resultArray['requiredFields'];
-               $this->requiredAdditional = $resultArray['requiredAdditional'];
-               $this->requiredNested = $resultArray['requiredNested'];
                $this->additionalJS_post = $resultArray['additionalJavaScriptPost'];
                $this->additionalJS_submit = $resultArray['additionalJavaScriptSubmit'];
                $this->extJSCODE = $resultArray['extJSCODE'];
@@ -382,18 +346,6 @@ class FormEngine {
         * @return void
         */
        protected function mergeResult(array $resultArray) {
-               foreach ($resultArray['requiredElements'] as $name => $element) {
-                       $this->requiredElements[$name] = $element;
-               }
-               foreach ($resultArray['requiredFields'] as $value => $name) {
-                       $this->requiredFields[$value] = $name;
-               }
-               foreach ($resultArray['requiredAdditional'] as $name => $subArray) {
-                       $this->requiredAdditional[$name] = $subArray;
-               }
-               foreach ($resultArray['requiredNested'] as $value => $name) {
-                       $this->requiredNested[$value] = $name;
-               }
                foreach ($resultArray['additionalJavaScriptPost'] as $element) {
                        $this->additionalJS_post[] = $element;
                }
@@ -553,10 +505,6 @@ class FormEngine {
                }
 
                // @todo: Refactor this mess ... see other methods like getMainFields, too
-               $this->requiredElements = $childArray['requiredElements'];
-               $this->requiredFields = $childArray['requiredFields'];
-               $this->requiredAdditional = $childArray['requiredAdditional'];
-               $this->requiredNested = $childArray['requiredNested'];
                $this->additionalJS_post = $childArray['additionalJavaScriptPost'];
                $this->additionalJS_submit = $childArray['additionalJavaScriptSubmit'];
                $this->extJSCODE = $childArray['extJSCODE'];
@@ -685,10 +633,6 @@ class FormEngine {
                }
 
                // @todo: Refactor this mess ... see other methods like getMainFields, too
-               $this->requiredElements = $childArray['requiredElements'];
-               $this->requiredFields = $childArray['requiredFields'];
-               $this->requiredAdditional = $childArray['requiredAdditional'];
-               $this->requiredNested = $childArray['requiredNested'];
                $this->additionalJS_post = $childArray['additionalJavaScriptPost'];
                $this->additionalJS_submit = $childArray['additionalJavaScriptSubmit'];
                $this->extJSCODE = $childArray['extJSCODE'];
@@ -798,18 +742,6 @@ class FormEngine {
                                        if (!empty($childArray['extJSCODE'])) {
                                                $resultArray['extJSCODE'] .= LF . $childArray['extJSCODE'];
                                        }
-                                       foreach ($childArray['requiredElements'] as $name => $value) {
-                                               $resultArray['requiredElements'][$name] = $value;
-                                       }
-                                       foreach ($childArray['requiredFields'] as $value => $name) { // Params swapped ?!
-                                               $resultArray['requiredFields'][$value] = $name;
-                                       }
-                                       foreach ($childArray['requiredAdditional'] as $name => $subArray) {
-                                               $resultArray['requiredAdditional'][$name] = $subArray;
-                                       }
-                                       foreach ($childArray['requiredNested'] as $value => $name) {
-                                               $resultArray['requiredNested'][$value] = $name;
-                                       }
                                        foreach ($childArray['additionalJavaScriptPost'] as $value) {
                                                $resultArray['additionalJavaScriptPost'][] = $value;
                                        }
@@ -836,10 +768,6 @@ class FormEngine {
                        }
 
                        // @todo: Refactor this mess ... see other methods like getMainFields, too
-                       $this->requiredElements = $resultArray['requiredElements'];
-                       $this->requiredFields = $resultArray['requiredFields'];
-                       $this->requiredAdditional = $resultArray['requiredAdditional'];
-                       $this->requiredNested = $resultArray['requiredNested'];
                        $this->additionalJS_post = $resultArray['additionalJavaScriptPost'];
                        $this->additionalJS_submit = $resultArray['additionalJavaScriptSubmit'];
                        $this->extJSCODE = $resultArray['extJSCODE'];
@@ -1255,30 +1183,7 @@ class FormEngine {
        public function JSbottom($formname = 'forms[0]', $update = FALSE) {
                $languageService = $this->getLanguageService();
                $jsFile = array();
-               $elements = array();
                $out = '';
-               // Required:
-               foreach ($this->requiredFields as $itemImgName => $itemName) {
-                       $match = array();
-                       if (preg_match('/^(.+)\\[((\\w|\\d|_)+)\\]$/', $itemName, $match)) {
-                               $record = $match[1];
-                               $field = $match[2];
-                               $elements[$record][$field]['required'] = 1;
-                               $elements[$record][$field]['requiredImg'] = $itemImgName;
-                               if (isset($this->requiredAdditional[$itemName]) && is_array($this->requiredAdditional[$itemName])) {
-                                       $elements[$record][$field]['additional'] = $this->requiredAdditional[$itemName];
-                               }
-                       }
-               }
-               // Range:
-               foreach ($this->requiredElements as $itemName => $range) {
-                       if (preg_match('/^(.+)\\[((\\w|\\d|_)+)\\]$/', $itemName, $match)) {
-                               $record = $match[1];
-                               $field = $match[2];
-                               $elements[$record][$field]['range'] = array($range[0], $range[1]);
-                               $elements[$record][$field]['rangeImg'] = $range['imgName'];
-                       }
-               }
                $this->TBE_EDITOR_fieldChanged_func = 'TBE_EDITOR.fieldChanged_fName(fName,formObj[fName+"_list"]);';
                if (!$update) {
                        if ($this->loadMD5_JS) {
@@ -1289,6 +1194,7 @@ class FormEngine {
                        $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/FormEngine', 'function(FormEngine) {
                                FormEngine.setBrowserUrl(' . GeneralUtility::quoteJSvalue(BackendUtility::getModuleUrl('browser')) . ');
                        }');
+                       $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/FormEngineValidation');
                        $pageRenderer->loadPrototype();
                        $pageRenderer->loadJquery();
                        $pageRenderer->loadExtJS();
@@ -1359,19 +1265,6 @@ class FormEngine {
                        inline.addToDataArray(' . json_encode($this->inlineData) . ');
                        ';
                }
-               // Registered nested elements for tabs or inline levels:
-               if (count($this->requiredNested)) {
-                       $out .= '
-                       TBE_EDITOR.addNested(' . json_encode($this->requiredNested) . ');
-                       ';
-               }
-               // Elements which are required or have a range definition:
-               if (count($elements)) {
-                       $out .= '
-                       TBE_EDITOR.addElements(' . json_encode($elements) . ');
-                       TBE_EDITOR.initRequired();
-                       ';
-               }
                // $this->additionalJS_submit:
                if ($this->additionalJS_submit) {
                        $additionalJS_submit = implode('', $this->additionalJS_submit);
index d88297b..0352053 100644 (file)
@@ -2,13 +2,12 @@
        <ul class="nav nav-tabs t3js-tabs" role="tablist" id="tabs-{id}" data-store-last-tab="{storeLastActiveTab}">
                <f:for each="{items}" as="item" iteration="iteration">
                        <f:if condition="{item.content}">
-                               <li role="presentation"{f:if(condition: '{iteration.cycle} == {defaultTabIndex}', then: ' class="active"')}>
+                               <li role="presentation" class="t3js-tabmenu-item {f:if(condition: '{iteration.cycle} == {defaultTabIndex}', then: ' active')}">
                                        <a href="#{id}-{iteration.cycle}" title="{item.linkTitle}" aria-controls="{id}-{iteration.cycle}" role="tab" data-toggle="tab">
                                                <f:if condition="{item.icon}">
                                                        <f:format.raw>{item.icon}</f:format.raw>
                                                </f:if>
                                                {item.label}
-                                               <img name="{id}-{iteration.cycle}-REQ" src="{BACK_PATH}gfx/clear.gif" class="t3-TCEforms-reqTabImg" alt="" />
                                                <f:if condition="{item.requiredIcon}">
                                                        <f:format.raw>{item.requiredIcon}</f:format.raw>
                                                </f:if>
index 298776a..5f5dafd 100644 (file)
@@ -171,6 +171,9 @@ define('TYPO3/CMS/Backend/FormEngine', ['jquery'], function ($) {
                        // Change the selected value
                        $fieldEl.val(value);
                }
+               if (typeof FormEngine.Validation !== 'undefinied' && typeof FormEngine.Validation.validate === 'function') {
+                       FormEngine.Validation.validate();
+               }
        };
 
        /**
@@ -771,6 +774,30 @@ define('TYPO3/CMS/Backend/FormEngine', ['jquery'], function ($) {
        };
 
        /**
+        * Show modal to confirm closing the document without saving
+        */
+       FormEngine.preventSaveIfHasErrors = function() {
+               if ($('.has-error').length > 0) {
+                       var title = TYPO3.lang['label.alert.save_with_error.title'] || 'You have errors in your form!';
+                       var content = TYPO3.lang['label.alert.save_with_error.content'] || 'Please check the form, there is at least one error in your form.';
+                       $modal = top.TYPO3.Modal.confirm(title, content, top.TYPO3.Severity.error, [
+                               {
+                                       text: TYPO3.lang['buttons.alert.save_with_error.ok'] || 'OK',
+                                       btnClass: 'btn-danger',
+                                       name: 'ok'
+                               }
+                       ]);
+                       $modal.on('button.clicked', function(e) {
+                               if (e.target.name === 'ok') {
+                                       top.TYPO3.Modal.dismiss();
+                               }
+                       });
+                       return false;
+               }
+               return true;
+       };
+
+       /**
         * Close current open document
         */
        FormEngine.closeDocument = function() {
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngineValidation.js b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngineValidation.js
new file mode 100644 (file)
index 0000000..c87947b
--- /dev/null
@@ -0,0 +1,185 @@
+/*
+ * 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!
+ */
+
+/**
+ * contains all JS functions related to TYPO3 TCEforms/FormEngineValidation
+ */
+define('TYPO3/CMS/Backend/FormEngineValidation', ['jquery', 'TYPO3/CMS/Backend/FormEngine'], function ($, FormEngine) {
+
+       /**
+        * the main FormEngineValidation object
+        *
+        * @type {{rulesSelector: string, dateTimeSelector: string, groupFieldHiddenElement: string, relatedFieldSelector: string, fieldsWithRules: null}}
+        */
+       var FormEngineValidation = {
+               rulesSelector: '[data-formengine-validation-rules]',
+               dateTimeSelector: '.t3js-datetimepicker',
+               groupFieldHiddenElement: '.t3js-formengine-field-group input[type=hidden]',
+               relatedFieldSelector: '[data-relatedfieldname]',
+               fieldsWithRules: null
+       };
+
+       /**
+        * initialize validation for the first time
+        */
+       FormEngineValidation.initialize = function() {
+               $(document).find('.has-error').removeClass('has-error');
+               FormEngineValidation.fieldsWithRules = $(document).find(FormEngineValidation.rulesSelector);
+
+               // bind to field changes
+               FormEngineValidation.fieldsWithRules.on('change', function() {
+                       // we need to wait, because the update of the select field needs some time
+                       window.setTimeout(function() {
+                               FormEngineValidation.validate();
+                       }, 500);
+               });
+
+               // bind to datepicker changes
+               $(document).on('dp.change', FormEngineValidation.dateTimeSelector, function(event) {
+                       FormEngineValidation.validate();
+               });
+
+               if (typeof RTEarea !== 'undefined') {
+                       console.log(RTEarea);
+               }
+       };
+
+       /**
+        * validate the complete form
+        */
+       FormEngineValidation.validate = function() {
+               FormEngineValidation.initialize();
+
+               $(document).find('.t3js-formengine-validation-marker, .t3js-tabmenu-item')
+                       .removeClass('has-error')
+                       .removeClass('has-validation-error');
+
+               FormEngineValidation.fieldsWithRules.each(function() {
+                       var $field = $(this);
+                       var $rules = $field.data('formengine-validation-rules');
+                       var markParent = false;
+                       $rules.each(function(rule) {
+                               switch (rule.type) {
+                                       case 'required':
+                                               if ($field.val() === '') {
+                                                       markParent = true;
+                                                       $field.closest('.t3js-formengine-validation-marker').addClass('has-error');
+                                               }
+                                               break;
+                                       case 'select':
+                                       case 'range':
+                                               if (rule.minItems || rule.maxItems) {
+                                                       $relatedField = $(document).find('[name="' + $field.data('relatedfieldname') + '"]');
+                                                       if ($relatedField.length) {
+                                                               var selected = FormEngineValidation.trimExplode(',', $relatedField.val()).length;
+                                                               if (selected < rule.minItems || selected > rule.maxItems) {
+                                                                       markParent = true;
+                                                                       $field.closest('.t3js-formengine-validation-marker').addClass('has-error');
+                                                               }
+                                                       } else {
+                                                               var selected = $field.val();
+                                                               if (selected < rule.minItems || selected > rule.maxItems) {
+                                                                       markParent = true;
+                                                                       $field.closest('.t3js-formengine-validation-marker').addClass('has-error');
+                                                               }
+
+                                                       }
+                                               }
+                                               break;
+                                       case 'group':
+                                               if (rule.minItems || rule.maxItems) {
+                                                       var selected = $field.find('option').length;
+                                                       if (selected < rule.minItems || selected > rule.maxItems) {
+                                                               markParent = true;
+                                                               $field.closest('.t3js-formengine-validation-marker').addClass('has-error');
+                                                       }
+                                               }
+                                               break;
+                                       case 'inline':
+                                               if (rule.minItems || rule.maxItems) {
+                                                       var selected = FormEngineValidation.trimExplode(',', $field.val()).length;
+                                                       if (selected < rule.minItems || selected > rule.maxItems) {
+                                                               markParent = true;
+                                                               $field.closest('.t3js-formengine-validation-marker').addClass('has-error');
+                                                       }
+                                               }
+                                               break;
+                                       default:
+                                               FormEngineValidation.log('unknown validation type: ' + rule.type);
+                               }
+                       });
+                       if (markParent) {
+                               // check tabs
+                               FormEngineValidation.markParentTab($field);
+                       }
+               });
+       };
+
+       /**
+        * helper function to get clean trimmed array from comma list
+        *
+        * @param delimiter
+        * @param string
+        * @returns {Array}
+        */
+       FormEngineValidation.trimExplode = function(delimiter, string) {
+               var result = [];
+               var items = string.split(delimiter);
+               for (var i=0; i<items.length; i++) {
+                       var item = items[i].trim();
+                       if (item.length > 0) {
+                               result.push(item);
+                       }
+               }
+               return result;
+       };
+
+       /**
+        * find tab by field and mark it as has-validation-error
+        *
+        * @param $element
+        */
+       FormEngineValidation.markParentTab = function($element) {
+               var $panes = $element.parents('.tab-pane');
+               $panes.each(function() {
+                       var $pane = $(this);
+                       var id = $pane.attr('id');
+                       $(document)
+                               .find('a[href="#' + id + '"]')
+                               .closest('.t3js-tabmenu-item')
+                               .addClass('has-validation-error');
+               });
+       };
+
+       /**
+        * helper function for console.log message
+        *
+        * @param msg
+        */
+       FormEngineValidation.log = function(msg) {
+               if (typeof console !== 'undefined') {
+                       console.log(msg);
+               }
+       };
+
+       /**
+        * initialize function
+        */
+       FormEngineValidation.initialize();
+       // Start first validation after one second, because all fields are initial empty (typo3form.fieldSet)
+       window.setTimeout(function() {
+               FormEngineValidation.validate();
+       }, 1000);
+
+       FormEngine.Validation = FormEngineValidation;
+});
index 18aab48..2d24a04 100644 (file)
@@ -287,6 +287,7 @@ var inline = {
                                });
                        }
                        TYPO3.FormEngine.reinitialize();
+                       TYPO3.FormEngine.Validation.validate();
                }
        },
 
@@ -1000,6 +1001,7 @@ var inline = {
                        this.showElementsWithClassName('.inlineNewFileUploadButton' + (md5 ? '.' + md5 : ''), objectParent);
                        this.showElementsWithClassName('.inlineForeignSelector' + (md5 ? '.'+md5 : ''), 't3-form-field-item');
                }
+               TYPO3.FormEngine.Validation.validate();
                return false;
        },
 
index 2ed2a99..ce56cd7 100644 (file)
@@ -18,7 +18,6 @@
  * @coauthor   Oliver Hader <oh@inpublica.de>
  */
 
-
 var TBE_EDITOR = {
        /* Example:
                elements: {
@@ -36,7 +35,6 @@ var TBE_EDITOR = {
        elements: {},
        nested: {'field':{}, 'level':{}},
        ignoreElements: [],
-       recentUpdatedElements: {},
        actionChecks: { submit: [] },
 
        formname: '',
@@ -59,51 +57,8 @@ var TBE_EDITOR = {
        clearBeforeSettingFormValueFromBrowseWin: [],
 
        // Handling of data structures:
-       addElements: function(elements) {
-               TBE_EDITOR.recentUpdatedElements = elements;
-               TBE_EDITOR.elements = $H(TBE_EDITOR.elements).merge(elements).toObject();
-       },
-       addNested: function(elements) {
-               // Merge data structures:
-               if (elements) {
-                       $H(elements).each(function(element) {
-                               var levelMax, i, currentLevel, subLevel;
-                               var nested = element.value;
-                               if (nested.level && nested.level.length) {
-                                               // If the first level is of type 'inline', it could be created by a AJAX request to IRRE.
-                                               // So, try to get the upper levels this dynamic level is nested in:
-                                       if (typeof inline!='undefined' && nested.level[0][0]=='inline') {
-                                               nested.level = inline.findContinuedNestedLevel(nested.level, nested.level[0][1]);
-                                       }
-                                       levelMax = nested.level.length-1;
-                                       for (i=0; i<=levelMax; i++) {
-                                               currentLevel = TBE_EDITOR.getNestedLevelIdent(nested.level[i]);
-                                               if (typeof TBE_EDITOR.nested.level[currentLevel] == 'undefined') {
-                                                       TBE_EDITOR.nested.level[currentLevel] = { 'clean': true, 'item': {}, 'sub': {} };
-                                               }
-                                                       // Add next sub level to the current level:
-                                               if (i<levelMax) {
-                                                       subLevel = TBE_EDITOR.getNestedLevelIdent(nested.level[i+1]);
-                                                       TBE_EDITOR.nested.level[currentLevel].sub[subLevel] = true;
-                                                       // Add the current item to the last level in nesting:
-                                               } else {
-                                                       TBE_EDITOR.nested.level[currentLevel].item[element.key] = nested.parts;
-                                               }
-                                       }
-                               }
-                       });
-                               // Merge the nested fields:
-                       TBE_EDITOR.nested.field = $H(TBE_EDITOR.nested.field).merge(elements).toObject();
-               }
-       },
        removeElement: function(record) {
                if (TBE_EDITOR.elements && TBE_EDITOR.elements[record]) {
-                               // Inform envolved levels the this record is removed and the missing requirements are resolved:
-                       $H(TBE_EDITOR.elements[record]).each(
-                               function(pair) {
-                                       TBE_EDITOR.notifyNested(record+'['+pair.key+']', true);
-                               }
-                       );
                        delete(TBE_EDITOR.elements[record]);
                }
        },
@@ -132,138 +87,7 @@ var TBE_EDITOR = {
                return result;
        },
        checkElements: function(type, recentUpdated, record, field) {
-               var result = 1;
-               var elementName, elementData, elementRecord, elementField;
-               var source = (recentUpdated ? TBE_EDITOR.recentUpdatedElements : TBE_EDITOR.elements);
-
-               if (TBE_EDITOR.ignoreElements.length && TBE_EDITOR.ignoreElements.indexOf(record)!=-1) {
-                       return result;
-               }
-
-               if (type) {
-                       if (record && field) {
-                               elementName = record+'['+field+']';
-                               elementData = TBE_EDITOR.getElement(record, field, type);
-                               if (elementData) {
-                                       if (!TBE_EDITOR.checkElementByType(type, elementName, elementData, recentUpdated)) {
-                                               result = 0;
-                                       }
-                               }
-
-                       } else {
-                               var elementFieldList, elRecIndex, elRecCnt, elFldIndex, elFldCnt;
-                               var elementRecordList = $H(source).keys();
-                               for (elRecIndex=0, elRecCnt=elementRecordList.length; elRecIndex<elRecCnt; elRecIndex++) {
-                                       elementRecord = elementRecordList[elRecIndex];
-                                       elementFieldList = $H(source[elementRecord]).keys();
-                                       for (elFldIndex=0, elFldCnt=elementFieldList.length; elFldIndex<elFldCnt; elFldIndex++) {
-                                               elementField = elementFieldList[elFldIndex];
-                                               elementData = TBE_EDITOR.getElement(elementRecord, elementField, type);
-                                               if (elementData) {
-                                                       elementName = elementRecord+'['+elementField+']';
-                                                       if (!TBE_EDITOR.checkElementByType(type, elementName, elementData, recentUpdated)) {
-                                                               result = 0;
-                                                       }
-                                               }
-                                       }
-                               }
-                       }
-               }
-
-               return result;
-       },
-       checkElementByType: function(type, elementName, elementData, autoNotify) {
-               var form, result = 1;
-
-               if (type) {
-                       if (type == 'required') {
-                               form = document[TBE_EDITOR.formname][elementName];
-                               if (form) {
-                                               // Check if we are within a deleted inline element
-                                       var testNode = $(form.parentNode);
-                                       while(testNode) {
-                                               if (testNode.hasClassName && testNode.hasClassName('inlineIsDeletedRecord')) {
-                                                       return result;
-                                               }
-                                               testNode = $(testNode.parentNode);
-                                       }
-
-                                       var value = form.value;
-                                       if (!value || elementData.additional && elementData.additional.isPositiveNumber && (isNaN(value) || Number(value) <= 0)) {
-                                               result = 0;
-                                               if (autoNotify) {
-                                                       TBE_EDITOR.setImage('req_'+elementData.requiredImg, TBE_EDITOR.images.req);
-                                                       TBE_EDITOR.notifyNested(elementName, false);
-                                               }
-                                       }
-                               }
-                       } else if (type == 'range' && elementData.range) {
-                               var numberOfElements = 0;
-                               form = document[TBE_EDITOR.formname][elementName+'_list'];
-                               if (!form) {
-                                               // special treatment for IRRE fields:
-                                       var tempObj = document[TBE_EDITOR.formname][elementName];
-                                       if (tempObj && (Element.hasClassName(tempObj, 'inlineRecord') || Element.hasClassName(tempObj, 'treeRecord'))) {
-                                               form = tempObj.value ? tempObj.value.split(',') : [];
-                                               numberOfElements = form.length;
-                                       }
-
-                               } else {
-                                               // special treatment for file uploads
-                                       var tempObj = document[TBE_EDITOR.formname][elementName.replace(/^data/, 'data_files') + '[]'];
-                                       numberOfElements = form.length;
-
-                                       if (tempObj && tempObj.type == 'file' && tempObj.value) {
-                                               numberOfElements++; // Add new uploaded file to the number of elements
-                                       }
-                               }
-
-                               if (!TBE_EDITOR.checkRange(numberOfElements, elementData.range[0], elementData.range[1])) {
-                                       result = 0;
-                                       if (autoNotify) {
-                                               TBE_EDITOR.setImage('req_'+elementData.rangeImg, TBE_EDITOR.images.req);
-                                               TBE_EDITOR.notifyNested(elementName, false);
-                                       }
-                               }
-                       }
-               }
-
-               return result;
-       },
-       // Notify tabs and inline levels with nested requiredFields/requiredElements:
-       notifyNested: function(elementName, resolved) {
-               if (TBE_EDITOR.nested.field[elementName]) {
-                       var i, nested, element, fieldLevels, fieldLevelIdent, nestedLevelType, nestedLevelName;
-                       fieldLevels = TBE_EDITOR.nested.field[elementName].level;
-                       TBE_EDITOR.nestedCache = {};
-
-                       for (i=fieldLevels.length-1; i>=0; i--) {
-                               nestedLevelType = fieldLevels[i][0];
-                               nestedLevelName = fieldLevels[i][1];
-                               fieldLevelIdent = TBE_EDITOR.getNestedLevelIdent(fieldLevels[i]);
-                                       // Construct the CSS id strings of the image/icon tags showing the notification:
-                               if (nestedLevelType == 'tab') {
-                                       element = nestedLevelName+'-REQ';
-                               } else if (nestedLevelType == 'inline') {
-                                       element = nestedLevelName+'_req';
-                               } else {
-                                       continue;
-                               }
-                                       // Set the icons:
-                               if (resolved) {
-                                       if (TBE_EDITOR.checkNested(fieldLevelIdent)) {
-                                               TBE_EDITOR.setImage(element, TBE_EDITOR.images.clear);
-                                       } else {
-                                               break;
-                                       }
-                               } else {
-                                       if (TBE_EDITOR.nested.level && TBE_EDITOR.nested.level[fieldLevelIdent]) {
-                                               TBE_EDITOR.nested.level[fieldLevelIdent].clean = false;
-                                       }
-                                       TBE_EDITOR.setImage(element, TBE_EDITOR.images.req);
-                               }
-                       }
-               }
+               return (document.getElementsByClassName('has-error').length == 0);
        },
        // Check all the input fields on a given level of nesting - if only on is unfilled, the whole level is marked as required:
        checkNested: function(nestedLevelIdent) {
@@ -304,9 +128,6 @@ var TBE_EDITOR = {
                }
                return true;
        },
-       getNestedLevelIdent: function(level) {
-               return level.join('::');
-       },
        addActionChecks: function(type, checks) {
                TBE_EDITOR.actionChecks[type].push(checks);
        },
@@ -337,22 +158,17 @@ var TBE_EDITOR = {
                if (TBE_EDITOR.getElement(theRecord,field,'required') && document[TBE_EDITOR.formname][theField]) {
                        if (TBE_EDITOR.checkElements('required', false, theRecord, field)) {
                                TBE_EDITOR.setImage(imgReqObjName,TBE_EDITOR.images.clear);
-                               TBE_EDITOR.notifyNested(theField, true);
                        } else {
                                TBE_EDITOR.setImage(imgReqObjName,TBE_EDITOR.images.req);
-                               TBE_EDITOR.notifyNested(theField, false);
                        }
                }
                if (TBE_EDITOR.getElement(theRecord,field,'range') && document[TBE_EDITOR.formname][theField]) {
                        if (TBE_EDITOR.checkElements('range', false, theRecord, field)) {
                                TBE_EDITOR.setImage(imgReqObjName,TBE_EDITOR.images.clear);
-                               TBE_EDITOR.notifyNested(theField, true);
                        } else {
                                TBE_EDITOR.setImage(imgReqObjName,TBE_EDITOR.images.req);
-                               TBE_EDITOR.notifyNested(theField, false);
                        }
                }
-
                if (TBE_EDITOR.isPalettedoc) { TBE_EDITOR.setOriginalFormFieldValue(theField) };
        },
        setOriginalFormFieldValue: function(theField) {
@@ -419,13 +235,6 @@ var TBE_EDITOR = {
                        return false;
                }
        },
-       initRequired: function() {
-               // $reqLinesCheck
-               TBE_EDITOR.checkElements('required', true);
-
-               // $reqRangeCheck
-               TBE_EDITOR.checkElements('range', true);
-       },
        setImage: function(name,image) {
                var object;
                if (document[name]) {
@@ -551,7 +360,6 @@ var TBE_EDITOR_isFormChanged = TBE_EDITOR.isFormChanged;
 var TBE_EDITOR_checkAndDoSubmit = TBE_EDITOR.checkAndDoSubmit;
 var TBE_EDITOR_checkSubmit = TBE_EDITOR.checkSubmit;
 var TBE_EDITOR_checkRange = TBE_EDITOR.checkRange;
-var TBE_EDITOR_initRequired = TBE_EDITOR.initRequired;
 var TBE_EDITOR_setImage = TBE_EDITOR.setImage;
 var TBE_EDITOR_submitForm = TBE_EDITOR.submitForm;
 var TBE_EDITOR_split = TBE_EDITOR.split;
index f4844c8..e166dca 100644 (file)
@@ -70,7 +70,7 @@
                                <source>Sorry, you didn't have proper permissions to perform this change.</source>
                        </trans-unit>
                        <trans-unit id="labels.fieldsMissing">
-                               <source>The fields marked with a yellow exclamation mark are not yet correctly filled in. Please complete them properly.</source>
+                               <source>The fields marked with an exclamation mark are not yet correctly filled in. Please complete them properly.</source>
                        </trans-unit>
                        <trans-unit id="labels.fieldsChanged" xml:space="preserve">
                                <source>There are unsaved changes in the form!
index 530800a..e0fd308 100644 (file)
@@ -41,18 +41,7 @@ class RichTextElement extends AbstractFormElement {
                $resultArray = $this->initializeResultArray();
                $backendUser = $this->getBackendUserAuthentication();
 
-               $evalList = GeneralUtility::trimExplode(',', $parameterArray['fieldConf']['config']['eval'], TRUE);
-               if (in_array('required', $evalList, TRUE)) {
-                       $resultArray['requiredFields'][$table . '_' . $row['uid'] . '_' . $fieldName] = $parameterArray['itemFormElName'];
-                       $tabAndInlineStack = $this->globalOptions['tabAndInlineStack'];
-                       if (!empty($tabAndInlineStack) && preg_match('/^(.+\\])\\[(\\w+)\\]$/', $parameterArray['itemFormElName'], $match)) {
-                               array_shift($match);
-                               $resultArray['requiredNested'][$parameterArray['itemFormElName']] = array(
-                                       'parts' => $match,
-                                       'level' => $tabAndInlineStack,
-                               );
-                       }
-               }
+               $validationConfig = array();
 
                // "Extra" configuration; Returns configuration for the field based on settings found in the "types" fieldlist. Traditionally, this is where RTE configuration has been found.
                $specialConfiguration = BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras']);
@@ -82,7 +71,8 @@ class RichTextElement extends AbstractFormElement {
                        '',
                        $tsConfigPid,
                        $this->globalOptions,
-                       $this->initializeResultArray()
+                       $this->initializeResultArray(),
+                       $this->getValidationDataAsDataAttribute($validationConfig)
                );
                // This is a compat layer for "other" RTE's: If the result is not an array, it is the html string,
                // otherwise it is a structure similar to our casual return array
index d107401..d10cd11 100644 (file)
@@ -325,9 +325,10 @@ class RteHtmlAreaBase extends \TYPO3\CMS\Backend\Rte\AbstractRte {
         * @param int $thePidValue PID value of record (true parent page id)
         * @param array $globalOptions Global options like 'readonly' for all elements. This is a hack until RTE is an own type
         * @param array $resultArray Initialized final result array that is returned later filled with content. This is a hack until RTE is an own type
+        * @param string $validatationDataAttribute the validation data attribute.
         * @return string HTML code for RTE!
         */
-       public function drawRTE($_, $table, $field, $row, $PA, $specConf, $thisConfig, $RTEtypeVal, $RTErelPath, $thePidValue, $globalOptions, $resultArray) {
+       public function drawRTE($_, $table, $field, $row, $PA, $specConf, $thisConfig, $RTEtypeVal, $RTErelPath, $thePidValue, $globalOptions, $resultArray, $validatationDataAttribute = '') {
                $languageService = $this->getLanguageService();
                $backendUser = $this->getBackendUserAuthentication();
                $database = $this->getDatabaseConnection();
@@ -492,7 +493,7 @@ class RteHtmlAreaBase extends \TYPO3\CMS\Backend\Rte\AbstractRte {
                $item = $this->triggerField($PA['itemFormElName']) . '
                        <div id="pleasewait' . $textAreaId . '" class="pleasewait" style="display: block;" >' . $languageService->getLL('Please wait') . '</div>
                        <div id="editorWrap' . $textAreaId . '" class="editorWrap" style="visibility: hidden; width:' . $editorWrapWidth . '; height:' . $editorWrapHeight . ';">
-                       <textarea id="RTEarea' . $textAreaId . '" name="' . htmlspecialchars($PA['itemFormElName']) . '" rows="0" cols="0" style="' . htmlspecialchars($this->RTEdivStyle, ENT_COMPAT, 'UTF-8', FALSE) . '">' . GeneralUtility::formatForTextarea($value) . '</textarea>
+                       <textarea id="RTEarea' . $textAreaId . '" ' . $validatationDataAttribute . ' name="' . htmlspecialchars($PA['itemFormElName']) . '" rows="0" cols="0" style="' . htmlspecialchars($this->RTEdivStyle, ENT_COMPAT, 'UTF-8', FALSE) . '">' . GeneralUtility::formatForTextarea($value) . '</textarea>
                        </div>' . LF;
 
                $resultArray['html'] = $item;
index 3ab849b..c9b0709 100644 (file)
@@ -2,32 +2,83 @@
 // Tab menu
 //
 .nav-tabs {
+       border-radius: @border-radius-base @border-radius-base 0 0;
        background-color: darken(@nav-tabs-active-link-bg, 5%);
        > li {
+               + li {
+                       margin-left: 2px;
+               }
                > a {
                        // Hotfix to prevent prototype / scriptaculous hiding the links
                        // this can be removed after prototype / scriptaculous removed completely.
                        // at the moment prototype / scriptaculous hide each tab after clicking on it.
                        // at the moment this seems to be the only way to fix the wrong behavior.
                        display: block!important;
-
-                       margin-right: 3px;
+                       margin-right: 0;
+                       .transition(all 0.25s ease-in-out);
+                       &:focus,
                        &:hover {
+                               border-color: @nav-tabs-link-hover-border-color @nav-tabs-link-hover-border-color @nav-tabs-border-color;
                                background: @nav-tabs-link-hover-bg;
                        }
                }
-               &.active > a {
-                       &,
-                       &:focus {
-                               background: @nav-tabs-active-link-bg;
+               &[class*="has-"] > a {
+                       &:before {
+                               font-family: FontAwesome;
+                               margin-right: 2px;
+                               margin-top: -2px;
+                               vertical-align: middle;
+                               font-size: 10px;
+                               text-align: center;
+                               background-color: rgba(255, 255, 255, 0.25);
+                               border-radius: 50%;
+                               width: 15px;
+                               height: 15px;
+                               display: inline-block;
+                               .transition(all 0.25s ease-in-out);
                        }
-                       &:hover {
-                               background: @nav-tabs-active-link-hover-bg;
+               }
+               // Has error state
+               &.has-validation-error {
+                       > a {
+                               background-color: @brand-danger;
+                               border-color: darken(@brand-danger, 10%);
+                               color: #fff;
+                               &:focus,
+                               &:hover {
+                                       color: #fff;
+                                       background-color: lighten(@brand-danger, 5%);
+                                       border-color: darken(@brand-danger, 5%);
+                               }
+                               &:before {
+                                       content: @fa-var-exclamation;
+                               }
+                       }
+                       &.active > a {
+                               color: inherit;
+                               background-color: @nav-tabs-active-link-bg;
+                               &:before {
+                                       background-color: @brand-danger;
+                                       color: #ffffff;
+                               }
                        }
                }
+               // Active state
+               &.active {
+                       > a,
+                       > a:focus,
+                       > a:active,
+                       > a:hover {
+                               border: 1px solid @nav-tabs-active-link-hover-border-color;
+                               border-bottom-color: @nav-tabs-active-link-bg;
+                               background-color: @nav-tabs-active-link-bg;
+                       }
+               }
+
        }
 }
 
+
 //
 // Hotfix display tab-panes always to prevent RTE initialisation problems
 //
index 2014e4f..0cc6a26 100644 (file)
@@ -35,10 +35,14 @@ span.typo3-moduleHeader img {
 //
 .has-change {
        .form-control-validation(@state-info-text; @state-info-text; @state-info-bg);
-
        .thumbnail-status {
                border: 1px solid @state-info-text;
        }
+       // A loading order issue prevents .has-change to be overridden with .has-error
+       // this is a workaround needs to be cleaned up in a less file restructuring
+       &.has-error {
+               .has-error;
+       }
 }
 
 //
@@ -80,6 +84,41 @@ span.typo3-moduleHeader img {
        }
 }
 
+
+//
+// Form group validation states
+//
+.form-group.has-error {
+       label:before {
+               font-family: FontAwesome;
+               font-size: 12px;
+               margin-right: 5px;
+               text-align: center;
+               content: @fa-var-warning;
+               color: @brand-danger;
+               display: inline-block;
+       }
+
+       .input-group-btn {
+               label {
+                       border-color: @brand-danger;
+                       .t3-icon {
+                               color: @brand-danger;
+                       }
+               }
+               label:before {
+                       font-family: inherit;
+                       font-size: inherit;
+                       margin-right: inherit;
+                       text-align: inherit;
+                       content: '';
+                       color: inherit;
+                       display: block;
+               }
+       }
+}
+
+
 //
 // Select
 //
index 6ae20ba..ced5bde 100644 (file)
@@ -9977,21 +9977,69 @@ span.spinner {
   background-image: url('../../../../images/spinner/f1f1f1.gif');
 }
 .nav-tabs {
+  border-radius: 2px 2px 0 0;
   background-color: #ededed;
 }
+.nav-tabs > li + li {
+  margin-left: 2px;
+}
 .nav-tabs > li > a {
   display: block!important;
-  margin-right: 3px;
+  margin-right: 0;
+  -webkit-transition: all 0.25s ease-in-out;
+  -o-transition: all 0.25s ease-in-out;
+  transition: all 0.25s ease-in-out;
 }
+.nav-tabs > li > a:focus,
 .nav-tabs > li > a:hover {
+  border-color: #d7d7d7 #d7d7d7 #cccccc;
   background: #e1e1e1;
 }
-.nav-tabs > li.active > a,
-.nav-tabs > li.active > a:focus {
-  background: #fafafa;
+.nav-tabs > li[class*="has-"] > a:before {
+  font-family: FontAwesome;
+  margin-right: 2px;
+  margin-top: -2px;
+  vertical-align: middle;
+  font-size: 10px;
+  text-align: center;
+  background-color: rgba(255, 255, 255, 0.25);
+  border-radius: 50%;
+  width: 15px;
+  height: 15px;
+  display: inline-block;
+  -webkit-transition: all 0.25s ease-in-out;
+  -o-transition: all 0.25s ease-in-out;
+  transition: all 0.25s ease-in-out;
 }
+.nav-tabs > li.has-validation-error > a {
+  background-color: #c83c3c;
+  border-color: #a32e2e;
+  color: #fff;
+}
+.nav-tabs > li.has-validation-error > a:focus,
+.nav-tabs > li.has-validation-error > a:hover {
+  color: #fff;
+  background-color: #ce5050;
+  border-color: #b73434;
+}
+.nav-tabs > li.has-validation-error > a:before {
+  content: "\f12a";
+}
+.nav-tabs > li.has-validation-error.active > a {
+  color: inherit;
+  background-color: #fafafa;
+}
+.nav-tabs > li.has-validation-error.active > a:before {
+  background-color: #c83c3c;
+  color: #ffffff;
+}
+.nav-tabs > li.active > a,
+.nav-tabs > li.active > a:focus,
+.nav-tabs > li.active > a:active,
 .nav-tabs > li.active > a:hover {
-  background: #ededed;
+  border: 1px solid #cccccc;
+  border-bottom-color: #fafafa;
+  background-color: #fafafa;
 }
 .tab-content > .tab-pane {
   display: block;
@@ -11754,6 +11802,36 @@ span.typo3-moduleHeader img {
 .has-change .thumbnail-status {
   border: 1px solid #6daae0;
 }
+.has-change.has-error .help-block,
+.has-change.has-error .control-label,
+.has-change.has-error .radio,
+.has-change.has-error .checkbox,
+.has-change.has-error .radio-inline,
+.has-change.has-error .checkbox-inline,
+.has-change.has-error.radio label,
+.has-change.has-error.checkbox label,
+.has-change.has-error.radio-inline label,
+.has-change.has-error.checkbox-inline label {
+  color: #c83c3c;
+}
+.has-change.has-error .form-control {
+  border-color: #c83c3c;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+.has-change.has-error .form-control:focus {
+  border-color: #a32e2e;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #de8c8c;
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #de8c8c;
+}
+.has-change.has-error .input-group-addon {
+  color: #c83c3c;
+  border-color: #c83c3c;
+  background-color: #efc7c7;
+}
+.has-change.has-error .form-control-feedback {
+  color: #c83c3c;
+}
 .input-group-icon {
   width: 32px;
   vertical-align: middle;
@@ -11781,6 +11859,30 @@ span.typo3-moduleHeader img {
 .row > .form-group > .form-control-wrap {
   margin-bottom: 0;
 }
+.form-group.has-error label:before {
+  font-family: FontAwesome;
+  font-size: 12px;
+  margin-right: 5px;
+  text-align: center;
+  content: "\f071";
+  color: #c83c3c;
+  display: inline-block;
+}
+.form-group.has-error .input-group-btn label {
+  border-color: #c83c3c;
+}
+.form-group.has-error .input-group-btn label .t3-icon {
+  color: #c83c3c;
+}
+.form-group.has-error .input-group-btn label:before {
+  font-family: inherit;
+  font-size: inherit;
+  margin-right: inherit;
+  text-align: inherit;
+  content: '';
+  color: inherit;
+  display: block;
+}
 select.form-control[multiple],
 select.form-control[size] {
   min-height: 156px;