[TASK] FormEngine: Simplify placeholder and NULL handling 65/39365/8
authorChristian Kuhn <lolli@schwarzbu.ch>
Fri, 8 May 2015 13:26:26 +0000 (15:26 +0200)
committerWouter Wolters <typo3@wouterwolters.nl>
Fri, 8 May 2015 22:23:28 +0000 (00:23 +0200)
FormEngine / DataHandler have logic to distinguish between NULL and
empty strings as values written to the database. This is especially
used for FAL in inline relation together with the placeholder logic.
The patch moves the code around a bit, disentangles the logic and
adds comments to explain the details. The $globalOptions value
parameter "prependFormFieldNamesActive" becomes obsolete along the
way and is dropped.

Change-Id: I854a5733f9b597a28751ed31217858b223441247
Resolves: #66856
Releases: master
Reviewed-on: http://review.typo3.org/39365
Reviewed-by: Frank Nägler <typo3@naegler.net>
Tested-by: Frank Nägler <typo3@naegler.net>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters <typo3@wouterwolters.nl>
typo3/sysext/backend/Classes/Form/Container/PaletteAndSingleContainer.php
typo3/sysext/backend/Classes/Form/Container/SingleFieldContainer.php
typo3/sysext/backend/Classes/Form/FormEngine.php

index 18d4c91..07a7e2e 100644 (file)
@@ -217,7 +217,7 @@ class PaletteAndSingleContainer extends AbstractContainer {
                                if ($this->isUserNoTableWrappingField($element)) {
                                        $content[] = $element['fieldHtml'];
                                } else {
-                                       $content[] = $this->fieldSetWrap($this->wrapSingleFieldContent($element));
+                                       $content[] = $this->fieldSetWrap($this->wrapSingleFieldContentWithLabelAndOuterDiv($element));
                                }
                        }
                }
@@ -352,7 +352,7 @@ class PaletteAndSingleContainer extends AbstractContainer {
                                                $result[] = '<div class="clearfix"></div>';
                                        }
                                } else {
-                                       $result[] = $this->wrapSingleFieldContent($element, array($colClass));
+                                       $result[] = $this->wrapSingleFieldContentWithLabelAndOuterDiv($element, array($colClass));
 
                                        // Breakpoints
                                        if ($counter + 1 < $numberOfItems && !empty($colClear)) {
@@ -423,7 +423,7 @@ class PaletteAndSingleContainer extends AbstractContainer {
         * @param array $additionalPaletteClasses Additional classes to be added to HTML
         * @return string Wrapped element
         */
-       protected function wrapSingleFieldContent(array $element, array $additionalPaletteClasses = array()) {
+       protected function wrapSingleFieldContentWithLabelAndOuterDiv(array $element, array $additionalPaletteClasses = array()) {
                $fieldName = $element['fieldName'];
 
                $paletteFieldClasses = array(
@@ -434,14 +434,6 @@ class PaletteAndSingleContainer extends AbstractContainer {
                        $paletteFieldClasses[] = $class;
                }
 
-               $fieldItemClasses = array(
-                       't3js-formengine-field-item'
-               );
-               $isNullValueField = $this->isDisabledNullValueField($fieldName);
-               if ($isNullValueField) {
-                       $fieldItemClasses[] = 'disabled';
-               }
-
                $label = BackendUtility::wrapInHelp($this->globalOptions['table'], $fieldName, htmlspecialchars($element['fieldLabel']));
 
                $content = array();
@@ -450,11 +442,7 @@ class PaletteAndSingleContainer extends AbstractContainer {
                $content[] =            $label;
                $content[] =            '<img name="req_' . $this->globalOptions['table'] . '_' . $this->globalOptions['databaseRow']['uid'] . '_' . $fieldName . '" src="clear.gif" class="t3js-formengine-field-required" alt="" />';
                $content[] =    '</label>';
-               $content[] =    '<div class="' . implode(' ', $fieldItemClasses) . '">';
-               $content[] =            '<div class="t3-form-field-disable"></div>';
-               $content[] =            $this->renderNullValueWidget($fieldName);
-               $content[] =            $element['fieldHtml'];
-               $content[] =    '</div>';
+               $content[] =    $element['fieldHtml'];
                $content[] = '</div>';
 
                return implode(LF, $content);
@@ -506,68 +494,6 @@ class PaletteAndSingleContainer extends AbstractContainer {
        }
 
        /**
-        * Determines whether the current field value is considered as NULL value.
-        * Using NULL values is enabled by using 'null' in the 'eval' TCA definition.
-        * If NULL value is possible for a field and additional checkbox next to the element will be rendered.
-        *
-        * @param string $fieldName The field to handle
-        * @return bool
-        */
-       protected function isDisabledNullValueField($fieldName) {
-               $table = $this->globalOptions['table'];
-               $row = $this->globalOptions['databaseRow'];
-               $config = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
-               $result = FALSE;
-               $value = $row[$fieldName];
-               if (
-                       $value === NULL
-                       && !empty($config['eval']) && GeneralUtility::inList($config['eval'], 'null')
-                       && (empty($config['mode']) || $config['mode'] !== 'useOrOverridePlaceholder')
-               ) {
-                       $result = TRUE;
-               }
-               return $result;
-       }
-
-       /**
-        * Renders a view widget to handle and activate NULL values.
-        * The widget is enabled by using 'null' in the 'eval' TCA definition.
-        *
-        * @param string $fieldName The field to handle
-        * @return string
-        */
-       protected function renderNullValueWidget($fieldName) {
-               $table = $this->globalOptions['table'];
-               $row = $this->globalOptions['databaseRow'];
-               $value = $row[$fieldName];
-               $config = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
-
-               $widget = array();
-               // Checkbox should be rendered if eval null set and no override stuff is done
-               if (
-                       !empty($config['eval']) && GeneralUtility::inList($config['eval'], 'null')
-                       && (empty($config['mode']) || $config['mode'] !== 'useOrOverridePlaceholder')
-               ) {
-                       $checked = $value === NULL ? '' : ' checked="checked"';
-                       $formElementName = $this->globalOptions['prependFormFieldNames'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
-                       $formElementNameActive = $this->globalOptions['prependFormFieldNamesActive'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
-                       $onChange = htmlspecialchars(
-                               'typo3form.fieldSetNull(\'' . $formElementName . '\', !this.checked)'
-                       );
-
-                       $widget = array();
-                       $widget[] = '<div class="checkbox">';
-                       $widget[] =     '<label>';
-                       $widget[] =             '<input type="hidden" name="' . $formElementNameActive . '" value="0" />';
-                       $widget[] =             '<input type="checkbox" name="' . $formElementNameActive . '" value="1" onchange="' . $onChange . '"' . $checked . ' /> &nbsp;';
-                       $widget[] =     '</label>';
-                       $widget[] = '</div>';
-               }
-
-               return implode(LF, $widget);
-       }
-
-       /**
         * @return LanguageService
         */
        protected function getLanguageService() {
index 4507742..14e9bcf 100644 (file)
@@ -92,9 +92,6 @@ class SingleFieldContainer extends AbstractContainer {
                $parameterArray['itemFormElName'] = $this->globalOptions['prependFormFieldNames'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
                // Form field name, in case of file uploads
                $parameterArray['itemFormElName_file'] = $this->globalOptions['prependFormFieldNames_file'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
-               // Form field name, to activate elements
-               // If the "eval" list contains "null", elements can be deactivated which results in storing NULL to database
-               $parameterArray['itemFormElNameActive'] = $this->globalOptions['prependFormFieldNamesActive'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
                $parameterArray['itemFormElID'] = $this->globalOptions['prependFormFieldNames'] . '_' . $table . '_' . $row['uid'] . '_' . $fieldName;
 
                // The value to show in the form field.
@@ -169,6 +166,10 @@ class SingleFieldContainer extends AbstractContainer {
                        $resultArray = $childElement->setGlobalOptions($options)->render();
                        $html = $resultArray['html'];
 
+                       // @todo: the language handling, the null and the placeholder stuff should be embedded in the single
+                       // @todo: element classes. Basically, this method should return here and have the element classes
+                       // @todo: decide on language stuff and other wraps already.
+
                        // Add language + diff
                        $renderLanguageDiff = TRUE;
                        if (
@@ -182,7 +183,63 @@ class SingleFieldContainer extends AbstractContainer {
                                $html = $this->renderDefaultLanguageDiff($table, $fieldName, $row, $html);
                        }
 
-                       if (isset($parameterArray['fieldConf']['config']['mode']) && $parameterArray['fieldConf']['config']['mode'] === 'useOrOverridePlaceholder') {
+                       $fieldItemClasses = array(
+                               't3js-formengine-field-item'
+                       );
+
+                       // NULL value and placeholder handling
+                       $nullControlNameAttribute = ' name="' . htmlspecialchars('control[active][' . $table . '][' . $row['uid'] . '][' . $fieldName . ']') . '"';
+                       if (!empty($parameterArray['fieldConf']['config']['eval']) && GeneralUtility::inList($parameterArray['fieldConf']['config']['eval'], 'null')
+                               && (empty($parameterArray['fieldConf']['config']['mode']) || $parameterArray['fieldConf']['config']['mode'] !== 'useOrOverridePlaceholder')
+                       ) {
+                               // This field has eval=null set, but has no useOverridePlaceholder defined.
+                               // Goal is to have a field that can distinct between NULL and empty string in the database.
+                               // A checkbox and an additional hidden field will be created, both with the same name
+                               // and prefixed with "control[active]". If the checkbox is set (value 1), the value from the casual
+                               // input field will be written to the database. If the checkbox is not set, the hidden field
+                               // transfers value=0 to DataHandler, the value of the input field will then be reset to NULL by the
+                               // DataHandler at an early point in processing, so NULL will be written to DB as field value.
+
+                               // If the value of the field *is* NULL at the moment, an additional class is set
+                               // @todo: This does not work well at the moment, but is kept for now. see input_14 of ext:styleguide as example
+                               $checked = ' checked="checked"';
+                               if ($this->globalOptions['databaseRow'][$fieldName] === NULL) {
+                                       $fieldItemClasses[] = 'disabled';
+                                       $checked = '';
+                               }
+
+                               $formElementName = $this->globalOptions['prependFormFieldNames'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
+                               $onChange = htmlspecialchars(
+                                       'typo3form.fieldSetNull(' . GeneralUtility::quoteJSvalue($formElementName) . ', !this.checked)'
+                               );
+
+                               $nullValueWrap = array();
+                               $nullValueWrap[] = '<div class="' . implode(' ', $fieldItemClasses) . '">';
+                               $nullValueWrap[] =      '<div class="t3-form-field-disable"></div>';
+                               $nullValueWrap[] =      '<div class="checkbox">';
+                               $nullValueWrap[] =              '<label>';
+                               $nullValueWrap[] =                      '<input type="hidden"' . $nullControlNameAttribute . ' value="0" />';
+                               $nullValueWrap[] =                      '<input type="checkbox"' . $nullControlNameAttribute . ' value="1" onchange="' . $onChange . '"' . $checked . ' /> &nbsp;';
+                               $nullValueWrap[] =              '</label>';
+                               $nullValueWrap[] =              $html;
+                               $nullValueWrap[] =      '</div>';
+                               $nullValueWrap[] = '</div>';
+
+                               $html = implode(LF, $nullValueWrap);
+                       } elseif (isset($parameterArray['fieldConf']['config']['mode']) && $parameterArray['fieldConf']['config']['mode'] === 'useOrOverridePlaceholder') {
+                               // This field has useOverridePlaceholder set.
+                               // Here, a value from a deeper DB structure can be "fetched up" as value, and can also be overridden by a
+                               // local value. This is used in FAL, where eg. the "title" field can have the default value from sys_file_metadata,
+                               // the title field of sys_file_reference is then set to NULL. Or the "override" checkbox is set, and a string
+                               // or an empty string is then written to the field of sys_file_reference.
+                               // The situation is similar to the NULL handling above, but additionally a "default" value should be shown.
+                               // To achieve this, again a hidden control[hidden] field is added together with a checkbox with the same name
+                               // to transfer the information whether the default value should be used or not: Checkbox checked transfers 1 as
+                               // value in control[active], meaning the overridden value should be used.
+                               // Additionally to the casual input field, a second field is added containing the "placeholder" value. This
+                               // field has no name attribute and is not transferred at all. Those two are then hidden / shown depending
+                               // on the state of the above checkbox in via JS.
+
                                $placeholder = $this->getPlaceholderValue($table, $parameterArray['fieldConf']['config'], $row);
                                $onChange = 'typo3form.fieldTogglePlaceholder(' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ', !this.checked)';
                                $checked = $parameterArray['itemFormElValue'] === NULL ? '' : ' checked="checked"';
@@ -201,18 +258,34 @@ class SingleFieldContainer extends AbstractContainer {
                                $noneElementResult = $noneElement->setGlobalOptions($options)->render();
                                $noneElementHtml = $noneElementResult['html'];
 
-                               $html = '
-                               <input type="hidden" name="' . htmlspecialchars($parameterArray['itemFormElNameActive']) . '" value="0" />
-                               <div class="checkbox">
-                                       <label>
-                                               <input type="checkbox" name="' . htmlspecialchars($parameterArray['itemFormElNameActive']) . '" value="1" id="tce-forms-textfield-use-override-' . $fieldName . '-' . $row['uid'] . '" onchange="' . htmlspecialchars($onChange) . '"' . $checked . ' />
-                                               ' . sprintf($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.placeholder.override'), BackendUtility::getRecordTitlePrep($placeholder, 20)) . '
-                                       </label>
-                               </div>
-                               <div class="t3js-formengine-placeholder-placeholder">
-                                       ' . $noneElementHtml . '
-                               </div>
-                               <div class="t3js-formengine-placeholder-formfield">' . $html . '</div>';
+                               $placeholderWrap = array();
+                               $placeholderWrap[] = '<div class="' . implode(' ', $fieldItemClasses) . '">';
+                               $placeholderWrap[] =    '<div class="t3-form-field-disable"></div>';
+                               $placeholderWrap[] =    '<div class="checkbox">';
+                               $placeholderWrap[] =            '<label>';
+                               $placeholderWrap[] =                    '<input type="hidden"' . $nullControlNameAttribute . ' value="0" />';
+                               $placeholderWrap[] =                    '<input type="checkbox"' . $nullControlNameAttribute . ' value="1" id="tce-forms-textfield-use-override-' . $fieldName . '-' . $row['uid'] . '" onchange="' . htmlspecialchars($onChange) . '"' . $checked . ' />';
+                               $placeholderWrap[] =                    sprintf($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.placeholder.override'), BackendUtility::getRecordTitlePrep($placeholder, 20));
+                               $placeholderWrap[] =            '</label>';
+                               $placeholderWrap[] =    '</div>';
+                               $placeholderWrap[] =    '<div class="t3js-formengine-placeholder-placeholder">';
+                               $placeholderWrap[] =            $noneElementHtml;
+                               $placeholderWrap[] =    '</div>';
+                               $placeholderWrap[] =    '<div class="t3js-formengine-placeholder-formfield">';
+                               $placeholderWrap[] =            $html;
+                               $placeholderWrap[] =    '</div>';
+                               $placeholderWrap[] = '</div>';
+
+                               $html = implode(LF, $placeholderWrap);
+                       } elseif ($parameterArray['fieldConf']['config']['type'] !== 'user' || empty($parameterArray['fieldConf']['config']['noTableWrapping'])) {
+                               // Add a casual wrap if the field is not of type user with no wrap requested.
+                               $standardWrap = array();
+                               $standardWrap[] = '<div class="' . implode(' ', $fieldItemClasses) . '">';
+                               $standardWrap[] =       '<div class="t3-form-field-disable"></div>';
+                               $standardWrap[] =       $html;
+                               $standardWrap[] = '</div>';
+
+                               $html = implode(LF, $standardWrap);
                        }
 
                        $resultArray['html'] = $html;
index 11bc4cc..bb65edb 100644 (file)
@@ -137,13 +137,6 @@ class FormEngine {
        public $prependFormFieldNames_file = 'data_files';
 
        /**
-        * The string to prepend form field names that are active (not NULL)
-        *
-        * @var string
-        */
-       protected $prependFormFieldNamesActive = 'control[active]';
-
-       /**
         * @var InlineStackProcessor
         */
        protected $inlineStackProcessor;
@@ -461,7 +454,6 @@ class FormEngine {
                        'localizationMode' => $this->localizationMode, // @todo: find out the details, Warning, this overlaps with inline behaviour localizationMode
                        'prependFormFieldNames' => $this->prependFormFieldNames,
                        'prependFormFieldNames_file' => $this->prependFormFieldNames_file,
-                       'prependFormFieldNamesActive' => $this->prependFormFieldNamesActive,
                        'prependCmdFieldNames' => $this->prependCmdFieldNames,
                        'tabAndInlineStack' => array(),
                        'inlineFirstPid' => $this->getInlineFirstPid(),
@@ -1284,17 +1276,7 @@ class FormEngine {
        }
 
        /**
-        * JavaScript code used for input-field evaluation.
-        *
-        * Example use:
-        *
-        * $msg .= 'Distribution time (hh:mm dd-mm-yy):<br /><input type="text" name="send_mail_datetime_hr"'
-        *         . ' onchange="typo3form.fieldGet(\'send_mail_datetime\', \'datetime\', \'\', 0,0);"'
-        *         . $this->getTBE()->formWidth(20) . ' /><input type="hidden" value="' . $GLOBALS['EXEC_TIME']
-        *         . '" name="send_mail_datetime" /><br />';
-        * $this->extJSCODE .= 'typo3form.fieldSet("send_mail_datetime", "datetime", "", 0,0);';
-        *
-        * ... and then include the result of this function after the form
+        * JavaScript bottom code
         *
         * @param string $formname The identification of the form on the page.
         * @param bool $update Just extend/update existing settings, e.g. for AJAX call