1<?php 2namespace TYPO3\CMS\Backend\Form\Element; 3 4/* 5 * This file is part of the TYPO3 CMS project. 6 * 7 * It is free software; you can redistribute it and/or modify it under 8 * the terms of the GNU General Public License, either version 2 9 * of the License, or any later version. 10 * 11 * For the full copyright and license information, please read the 12 * LICENSE.txt file that was distributed with this source code. 13 * 14 * The TYPO3 project - inspiring people to share! 15 */ 16 17use TYPO3\CMS\Core\Imaging\Icon; 18use TYPO3\CMS\Core\Localization\LanguageService; 19use TYPO3\CMS\Core\Utility\GeneralUtility; 20use TYPO3\CMS\Core\Utility\MathUtility; 21use TYPO3\CMS\Core\Utility\StringUtility; 22 23/** 24 * Generation of TCEform elements of the type "input type=text" 25 */ 26class InputDateTimeElement extends AbstractFormElement 27{ 28 /** 29 * Default field information enabled for this element. 30 * 31 * @var array 32 */ 33 protected $defaultFieldInformation = [ 34 'tcaDescription' => [ 35 'renderType' => 'tcaDescription', 36 ], 37 ]; 38 39 /** 40 * Default field wizards enabled for this element. 41 * 42 * @var array 43 */ 44 protected $defaultFieldWizard = [ 45 'localizationStateSelector' => [ 46 'renderType' => 'localizationStateSelector', 47 ], 48 'otherLanguageContent' => [ 49 'renderType' => 'otherLanguageContent', 50 'after' => [ 51 'localizationStateSelector' 52 ], 53 ], 54 'defaultLanguageDifferences' => [ 55 'renderType' => 'defaultLanguageDifferences', 56 'after' => [ 57 'otherLanguageContent', 58 ], 59 ], 60 ]; 61 62 /** 63 * This will render a single-line input form field, possibly with various control/validation features 64 * 65 * @return array As defined in initializeResultArray() of AbstractNode 66 * @throws \RuntimeException with invalid configuration 67 */ 68 public function render() 69 { 70 $languageService = $this->getLanguageService(); 71 72 $table = $this->data['tableName']; 73 $fieldName = $this->data['fieldName']; 74 $row = $this->data['databaseRow']; 75 $parameterArray = $this->data['parameterArray']; 76 $resultArray = $this->initializeResultArray(); 77 $config = $parameterArray['fieldConf']['config']; 78 79 $itemValue = $parameterArray['itemFormElValue']; 80 $defaultInputWidth = 10; 81 $evalList = GeneralUtility::trimExplode(',', $config['eval'], true); 82 $nullControlNameEscaped = htmlspecialchars('control[active][' . $table . '][' . $row['uid'] . '][' . $fieldName . ']'); 83 84 if (in_array('date', $evalList, true)) { 85 $format = 'date'; 86 $defaultInputWidth = 13; 87 } elseif (in_array('datetime', $evalList, true)) { 88 $format = 'datetime'; 89 $defaultInputWidth = 13; 90 } elseif (in_array('time', $evalList, true)) { 91 $format = 'time'; 92 } elseif (in_array('timesec', $evalList, true)) { 93 $format = 'timesec'; 94 } else { 95 throw new \RuntimeException( 96 'Field "' . $fieldName . '" in table "' . $table . '" with renderType "inputDataTime" needs' 97 . '"eval" set to either "date", "datetime", "time" or "timesec"', 98 1483823746 99 ); 100 } 101 102 $size = MathUtility::forceIntegerInRange($config['size'] ?? $defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth); 103 $width = (int)$this->formMaxWidth($size); 104 105 $fieldInformationResult = $this->renderFieldInformation(); 106 $fieldInformationHtml = $fieldInformationResult['html']; 107 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false); 108 109 // Early return for read only fields 110 if (isset($config['readOnly']) && $config['readOnly']) { 111 // Ensure dbType values (see DatabaseRowDateTimeFields) are converted to a UNIX timestamp before rendering read-only 112 if (!empty($itemValue) && !MathUtility::canBeInterpretedAsInteger($itemValue)) { 113 $itemValue = (new \DateTime($itemValue))->getTimestamp(); 114 } 115 // Format the unix-timestamp to the defined format (date/year etc) 116 $itemValue = $this->formatValue($format, $itemValue); 117 $html = []; 118 $html[] = '<div class="formengine-field-item t3js-formengine-field-item">'; 119 $html[] = $fieldInformationHtml; 120 $html[] = '<div class="form-wizards-wrap">'; 121 $html[] = '<div class="form-wizards-element">'; 122 $html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">'; 123 $html[] = '<input class="form-control" value="' . htmlspecialchars($itemValue) . '" type="text" disabled>'; 124 $html[] = '</div>'; 125 $html[] = '</div>'; 126 $html[] = '</div>'; 127 $html[] = '</div>'; 128 $resultArray['html'] = implode(LF, $html); 129 return $resultArray; 130 } 131 132 $attributes = [ 133 'value' => '', 134 'id' => StringUtility::getUniqueId('formengine-input-'), 135 'class' => implode(' ', [ 136 't3js-datetimepicker', 137 'form-control', 138 't3js-clearable', 139 'hasDefaultValue', 140 ]), 141 'data-date-type' => $format, 142 'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config), 143 'data-formengine-input-params' => json_encode([ 144 'field' => $parameterArray['itemFormElName'], 145 'evalList' => implode(',', $evalList) 146 ]), 147 'data-formengine-input-name' => $parameterArray['itemFormElName'], 148 ]; 149 150 $maxLength = $config['max'] ?? 0; 151 if ((int)$maxLength > 0) { 152 $attributes['maxlength'] = (int)$maxLength; 153 } 154 if (!empty($config['placeholder'])) { 155 $attributes['placeholder'] = trim($config['placeholder']); 156 } 157 158 if ($format === 'datetime' || $format === 'date') { 159 // This only handles integer timestamps; if the field is a SQL native date(time), it was already converted 160 // to an ISO-8601 date by the DatabaseRowDateTimeFields class. (those dates are stored as server local time) 161 if (MathUtility::canBeInterpretedAsInteger($itemValue) && $itemValue != 0) { 162 // We store UTC timestamps in the database. 163 // Convert the timestamp to a proper ISO-8601 date so we get rid of timezone issues on the client. 164 // Details: As the JS side is not capable of handling dates in the server's timezone 165 // (moment.js can only handle UTC or browser's local timezone), we need to offset the value 166 // to eliminate the timezone. JS will receive all dates as if they were UTC, which we undo on save in DataHandler 167 $adjustedValue = $itemValue + date('Z', (int)$itemValue); 168 // output date as a ISO-8601 date 169 $itemValue = gmdate('c', $adjustedValue); 170 } 171 if (isset($config['range']['lower'])) { 172 $attributes['data-date-min-date'] = (int)$config['range']['lower'] * 1000; 173 } 174 if (isset($config['range']['upper'])) { 175 $attributes['data-date-max-date'] = (int)$config['range']['upper'] * 1000; 176 } 177 } 178 if (($format === 'time' || $format === 'timesec') && MathUtility::canBeInterpretedAsInteger($itemValue) && $itemValue != 0) { 179 // time(sec) is stored as elapsed seconds in DB, hence we interpret it as UTC time on 1970-01-01 180 // and pass on the ISO format to JS. 181 $itemValue = gmdate('c', (int)$itemValue); 182 } 183 184 $fieldWizardResult = $this->renderFieldWizard(); 185 $fieldWizardHtml = $fieldWizardResult['html']; 186 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false); 187 188 $fieldControlResult = $this->renderFieldControl(); 189 $fieldControlHtml = $fieldControlResult['html']; 190 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false); 191 192 $expansionHtml = []; 193 $expansionHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">'; 194 $expansionHtml[] = '<div class="form-wizards-wrap">'; 195 $expansionHtml[] = '<div class="form-wizards-element">'; 196 $expansionHtml[] = '<div class="input-group">'; 197 $expansionHtml[] = '<input type="text" ' . GeneralUtility::implodeAttributes($attributes, true) . ' />'; 198 $expansionHtml[] = '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($itemValue) . '" />'; 199 $expansionHtml[] = '<span class="input-group-btn">'; 200 $expansionHtml[] = '<label class="btn btn-default" for="' . $attributes['id'] . '">'; 201 $expansionHtml[] = $this->iconFactory->getIcon('actions-edit-pick-date', Icon::SIZE_SMALL)->render(); 202 $expansionHtml[] = '</label>'; 203 $expansionHtml[] = '</span>'; 204 $expansionHtml[] = '</div>'; 205 $expansionHtml[] = '</div>'; 206 if (!empty($fieldControlHtml)) { 207 $expansionHtml[] = '<div class="form-wizards-items-aside">'; 208 $expansionHtml[] = '<div class="btn-group">'; 209 $expansionHtml[] = $fieldControlHtml; 210 $expansionHtml[] = '</div>'; 211 $expansionHtml[] = '</div>'; 212 } 213 if (!empty($fieldWizardHtml)) { 214 $expansionHtml[] = '<div class="form-wizards-items-bottom">'; 215 $expansionHtml[] = $fieldWizardHtml; 216 $expansionHtml[] = '</div>'; 217 } 218 $expansionHtml[] = '</div>'; 219 $expansionHtml[] = '</div>'; 220 $expansionHtml = implode(LF, $expansionHtml); 221 222 $fullElement = $expansionHtml; 223 if ($this->hasNullCheckboxButNoPlaceholder()) { 224 $checked = $itemValue !== null ? ' checked="checked"' : ''; 225 $fullElement = []; 226 $fullElement[] = '<div class="t3-form-field-disable"></div>'; 227 $fullElement[] = '<div class="checkbox t3-form-field-eval-null-checkbox">'; 228 $fullElement[] = '<label for="' . $nullControlNameEscaped . '">'; 229 $fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />'; 230 $fullElement[] = '<input type="checkbox" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />'; 231 $fullElement[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox'); 232 $fullElement[] = '</label>'; 233 $fullElement[] = '</div>'; 234 $fullElement[] = $expansionHtml; 235 $fullElement = implode(LF, $fullElement); 236 } elseif ($this->hasNullCheckboxWithPlaceholder()) { 237 $checked = $itemValue !== null ? ' checked="checked"' : ''; 238 $placeholder = $shortenedPlaceholder = $config['placeholder'] ?? ''; 239 $disabled = ''; 240 $fallbackValue = 0; 241 if (strlen($placeholder) > 0) { 242 $shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20); 243 if ($placeholder !== $shortenedPlaceholder) { 244 $overrideLabel = sprintf( 245 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'), 246 '<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>' 247 ); 248 } else { 249 $overrideLabel = sprintf( 250 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'), 251 htmlspecialchars($placeholder) 252 ); 253 } 254 } else { 255 $overrideLabel = $languageService->sL( 256 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available' 257 ); 258 } 259 $fullElement = []; 260 $fullElement[] = '<div class="checkbox t3js-form-field-eval-null-placeholder-checkbox">'; 261 $fullElement[] = '<label for="' . $nullControlNameEscaped . '">'; 262 $fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="' . $fallbackValue . '" />'; 263 $fullElement[] = '<input type="checkbox" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . $disabled . ' />'; 264 $fullElement[] = $overrideLabel; 265 $fullElement[] = '</label>'; 266 $fullElement[] = '</div>'; 267 $fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">'; 268 $fullElement[] = '<div class="form-control-wrap" style="max-width:' . $width . 'px">'; 269 $fullElement[] = '<input type="text" class="form-control" disabled="disabled" value="' . htmlspecialchars($shortenedPlaceholder) . '" />'; 270 $fullElement[] = '</div>'; 271 $fullElement[] = '</div>'; 272 $fullElement[] = '<div class="t3js-formengine-placeholder-formfield">'; 273 $fullElement[] = $expansionHtml; 274 $fullElement[] = '</div>'; 275 $fullElement = implode(LF, $fullElement); 276 } 277 278 $resultArray['html'] = '<div class="formengine-field-item t3js-formengine-field-item">' . $fieldInformationHtml . $fullElement . '</div>'; 279 return $resultArray; 280 } 281 282 /** 283 * @return LanguageService 284 */ 285 protected function getLanguageService(): LanguageService 286 { 287 return $GLOBALS['LANG']; 288 } 289} 290