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