3 namespace Drupal\Core\Datetime\Element;
5 use Drupal\Component\Utility\NestedArray;
6 use Drupal\Core\Datetime\DrupalDateTime;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\Core\Datetime\Entity\DateFormat;
11 * Provides a datetime element.
13 * @FormElement("datetime")
15 class Datetime extends DateElementBase {
18 * @var \DateTimeInterface
20 protected static $dateExample;
25 public function getInfo() {
28 // Date formats cannot be loaded during install or update.
29 if (!defined('MAINTENANCE_MODE')) {
30 if ($date_format_entity = DateFormat::load('html_date')) {
31 /** @var $date_format_entity \Drupal\Core\Datetime\DateFormatInterface */
32 $date_format = $date_format_entity->getPattern();
34 if ($time_format_entity = DateFormat::load('html_time')) {
35 /** @var $time_format_entity \Drupal\Core\Datetime\DateFormatInterface */
36 $time_format = $time_format_entity->getPattern();
40 $class = get_class($this);
43 '#element_validate' => [
44 [$class, 'validateDatetime'],
47 [$class, 'processDatetime'],
48 [$class, 'processAjaxForm'],
49 [$class, 'processGroup'],
52 [$class, 'preRenderGroup'],
54 '#theme' => 'datetime_form',
55 '#theme_wrappers' => ['datetime_wrapper'],
56 '#date_date_format' => $date_format,
57 '#date_date_element' => 'date',
58 '#date_date_callbacks' => [],
59 '#date_time_format' => $time_format,
60 '#date_time_element' => 'time',
61 '#date_time_callbacks' => [],
62 '#date_year_range' => '1900:2050',
63 '#date_increment' => 1,
64 '#date_timezone' => '',
71 public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
72 if ($input !== FALSE) {
73 $date_input = $element['#date_date_element'] != 'none' && !empty($input['date']) ? $input['date'] : '';
74 $time_input = $element['#date_time_element'] != 'none' && !empty($input['time']) ? $input['time'] : '';
75 $date_format = $element['#date_date_element'] != 'none' ? static::getHtml5DateFormat($element) : '';
76 $time_format = $element['#date_time_element'] != 'none' ? static::getHtml5TimeFormat($element) : '';
77 $timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
79 // Seconds will be omitted in a post in case there's no entry.
80 if (!empty($time_input) && strlen($time_input) == 5) {
85 $date_time_format = trim($date_format . ' ' . $time_format);
86 $date_time_input = trim($date_input . ' ' . $time_input);
87 $date = DrupalDateTime::createFromFormat($date_time_format, $date_time_input, $timezone);
89 catch (\Exception $e) {
93 'date' => $date_input,
94 'time' => $time_input,
99 $date = $element['#default_value'];
100 if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
102 'date' => $date->format($element['#date_date_format']),
103 'time' => $date->format($element['#date_time_format']),
119 * Expands a datetime element type into date and/or time elements.
121 * All form elements are designed to have sane defaults so any or all can be
122 * omitted. Both the date and time components are configurable so they can be
123 * output as HTML5 datetime elements or not, as desired.
125 * Examples of possible configurations include:
126 * HTML5 date and time:
127 * #date_date_element = 'date';
128 * #date_time_element = 'time';
130 * #date_date_element = 'datetime';
131 * #date_time_element = 'none';
133 * #date_date_element = 'none';
134 * #date_time_element = 'time'
136 * #date_date_element = 'text';
137 * #date_time_element = 'text';
140 * - #default_value: A DrupalDateTime object, adjusted to the proper local
141 * timezone. Converting a date stored in the database from UTC to the local
142 * zone and converting it back to UTC before storing it is not handled here.
143 * This element accepts a date as the default value, and then converts the
144 * user input strings back into a new date object on submission. No timezone
145 * adjustment is performed.
146 * Optional properties include:
147 * - #date_date_format: A date format string that describes the format that
148 * should be displayed to the end user for the date. When using HTML5
149 * elements the format MUST use the appropriate HTML5 format for that
150 * element, no other format will work. See the format_date() function for a
151 * list of the possible formats and HTML5 standards for the HTML5
152 * requirements. Defaults to the right HTML5 format for the chosen element
153 * if a HTML5 element is used, otherwise defaults to
154 * DateFormat::load('html_date')->getPattern().
155 * - #date_date_element: The date element. Options are:
156 * - datetime: Use the HTML5 datetime element type.
157 * - datetime-local: Use the HTML5 datetime-local element type.
158 * - date: Use the HTML5 date element type.
159 * - text: No HTML5 element, use a normal text field.
160 * - none: Do not display a date element.
161 * - #date_date_callbacks: Array of optional callbacks for the date element.
162 * Can be used to add a jQuery datepicker.
163 * - #date_time_element: The time element. Options are:
164 * - time: Use a HTML5 time element type.
165 * - text: No HTML5 element, use a normal text field.
166 * - none: Do not display a time element.
167 * - #date_time_format: A date format string that describes the format that
168 * should be displayed to the end user for the time. When using HTML5
169 * elements the format MUST use the appropriate HTML5 format for that
170 * element, no other format will work. See the format_date() function for
171 * a list of the possible formats and HTML5 standards for the HTML5
172 * requirements. Defaults to the right HTML5 format for the chosen element
173 * if a HTML5 element is used, otherwise defaults to
174 * DateFormat::load('html_time')->getPattern().
175 * - #date_time_callbacks: An array of optional callbacks for the time
176 * element. Can be used to add a jQuery timepicker or an 'All day' checkbox.
177 * - #date_year_range: A description of the range of years to allow, like
178 * '1900:2050', '-3:+3' or '2000:+3', where the first value describes the
179 * earliest year and the second the latest year in the range. A year
180 * in either position means that specific year. A +/- value describes a
181 * dynamic value that is that many years earlier or later than the current
182 * year at the time the form is displayed. Used in jQueryUI datepicker year
183 * range and HTML5 min/max date settings. Defaults to '1900:2050'.
184 * - #date_increment: The interval (step) to use when incrementing or
185 * decrementing time, in seconds. For example, if this value is set to 30,
186 * time increases (or decreases) in steps of 30 seconds (00:00:00,
187 * 00:00:30, 00:01:00, and so on.) If this value is a multiple of 60, the
188 * "seconds"-component will not be shown in the input. Used for HTML5 step
189 * values and jQueryUI datepicker settings. Defaults to 1 to show every
191 * - #date_timezone: The local timezone to use when creating dates. Generally
192 * this should be left empty and it will be set correctly for the user using
193 * the form. Useful if the default value is empty to designate a desired
194 * timezone for dates created in form processing. If a default date is
195 * provided, this value will be ignored, the timezone in the default date
196 * takes precedence. Defaults to the value returned by
197 * drupal_get_user_timezone().
202 * '#type' => 'datetime',
203 * '#default_value' => new DrupalDateTime('2000-01-01 00:00:00'),
204 * '#date_date_element' => 'date',
205 * '#date_time_element' => 'none',
206 * '#date_year_range' => '2010:+3',
210 * @param array $element
211 * The form element whose value is being processed.
212 * @param \Drupal\Core\Form\FormStateInterface $form_state
213 * The current state of the form.
214 * @param array $complete_form
215 * The complete form structure.
218 * The form element whose value has been processed.
220 public static function processDatetime(&$element, FormStateInterface $form_state, &$complete_form) {
221 $format_settings = [];
222 // The value callback has populated the #value array.
223 $date = !empty($element['#value']['object']) ? $element['#value']['object'] : NULL;
225 // Set a fallback timezone.
226 if ($date instanceof DrupalDateTime) {
227 $element['#date_timezone'] = $date->getTimezone()->getName();
229 elseif (empty($element['#timezone'])) {
230 $element['#date_timezone'] = drupal_get_user_timezone();
233 $element['#tree'] = TRUE;
235 if ($element['#date_date_element'] != 'none') {
237 $date_format = $element['#date_date_element'] != 'none' ? static::getHtml5DateFormat($element) : '';
238 $date_value = !empty($date) ? $date->format($date_format, $format_settings) : $element['#value']['date'];
240 // Creating format examples on every individual date item is messy, and
241 // placeholders are invalid for HTML5 date and datetime, so an example
242 // format is appended to the title to appear in tooltips.
243 $extra_attributes = [
244 'title' => t('Date (e.g. @format)', ['@format' => static::formatExample($date_format)]),
245 'type' => $element['#date_date_element'],
248 // Adds the HTML5 date attributes.
249 if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
250 $html5_min = clone($date);
251 $range = static::datetimeRangeYears($element['#date_year_range'], $date);
252 $html5_min->setDate($range[0], 1, 1)->setTime(0, 0, 0);
253 $html5_max = clone($date);
254 $html5_max->setDate($range[1], 12, 31)->setTime(23, 59, 59);
256 $extra_attributes += [
257 'min' => $html5_min->format($date_format, $format_settings),
258 'max' => $html5_max->format($date_format, $format_settings),
264 '#title' => t('Date'),
265 '#title_display' => 'invisible',
266 '#value' => $date_value,
267 '#attributes' => $element['#attributes'] + $extra_attributes,
268 '#required' => $element['#required'],
269 '#size' => max(12, strlen($element['#value']['date'])),
270 '#error_no_message' => TRUE,
271 '#date_date_format' => $element['#date_date_format'],
274 // Allows custom callbacks to alter the element.
275 if (!empty($element['#date_date_callbacks'])) {
276 foreach ($element['#date_date_callbacks'] as $callback) {
277 if (function_exists($callback)) {
278 $callback($element, $form_state, $date);
284 if ($element['#date_time_element'] != 'none') {
286 $time_format = $element['#date_time_element'] != 'none' ? static::getHtml5TimeFormat($element) : '';
287 $time_value = !empty($date) ? $date->format($time_format, $format_settings) : $element['#value']['time'];
289 // Adds the HTML5 attributes.
290 $extra_attributes = [
291 'title' => t('Time (e.g. @format)', ['@format' => static::formatExample($time_format)]),
292 'type' => $element['#date_time_element'],
293 'step' => $element['#date_increment'],
297 '#title' => t('Time'),
298 '#title_display' => 'invisible',
299 '#value' => $time_value,
300 '#attributes' => $element['#attributes'] + $extra_attributes,
301 '#required' => $element['#required'],
303 '#error_no_message' => TRUE,
306 // Allows custom callbacks to alter the element.
307 if (!empty($element['#date_time_callbacks'])) {
308 foreach ($element['#date_time_callbacks'] as $callback) {
309 if (function_exists($callback)) {
310 $callback($element, $form_state, $date);
322 public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) {
323 $element = parent::processAjaxForm($element, $form_state, $complete_form);
325 // Copy the #ajax settings to the child elements.
326 if (isset($element['#ajax'])) {
327 if (isset($element['date'])) {
328 $element['date']['#ajax'] = $element['#ajax'];
330 if (isset($element['time'])) {
331 $element['time']['#ajax'] = $element['#ajax'];
339 * Validation callback for a datetime element.
341 * If the date is valid, the date object created from the user input is set in
342 * the form for use by the caller. The work of compiling the user input back
343 * into a date object is handled by the value callback, so we can use it here.
344 * We also have the raw input available for validation testing.
346 * @param array $element
347 * The form element whose value is being validated.
348 * @param \Drupal\Core\Form\FormStateInterface $form_state
349 * The current state of the form.
350 * @param array $complete_form
351 * The complete form structure.
353 public static function validateDatetime(&$element, FormStateInterface $form_state, &$complete_form) {
354 $input_exists = FALSE;
355 $input = NestedArray::getValue($form_state->getValues(), $element['#parents'], $input_exists);
358 $title = !empty($element['#title']) ? $element['#title'] : '';
359 $date_format = $element['#date_date_element'] != 'none' ? static::getHtml5DateFormat($element) : '';
360 $time_format = $element['#date_time_element'] != 'none' ? static::getHtml5TimeFormat($element) : '';
361 $format = trim($date_format . ' ' . $time_format);
363 // If there's empty input and the field is not required, set it to empty.
364 if (empty($input['date']) && empty($input['time']) && !$element['#required']) {
365 $form_state->setValueForElement($element, NULL);
367 // If there's empty input and the field is required, set an error. A
368 // reminder of the required format in the message provides a good UX.
369 elseif (empty($input['date']) && empty($input['time']) && $element['#required']) {
370 $form_state->setError($element, t('The %field date is required. Please enter a date in the format %format.', ['%field' => $title, '%format' => static::formatExample($format)]));
373 // If the date is valid, set it.
374 $date = $input['object'];
375 if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
376 $form_state->setValueForElement($element, $date);
378 // If the date is invalid, set an error. A reminder of the required
379 // format in the message provides a good UX.
381 $form_state->setError($element, t('The %field date is invalid. Please enter a date in the format %format.', ['%field' => $title, '%format' => static::formatExample($format)]));
388 * Creates an example for a date format.
390 * This is centralized for a consistent method of creating these examples.
392 * @param string $format
396 public static function formatExample($format) {
397 if (!static::$dateExample) {
398 static::$dateExample = new DrupalDateTime();
400 return static::$dateExample->format($format);
404 * Retrieves the right format for a HTML5 date element.
406 * The format is important because these elements will not work with any other
409 * @param string $element
410 * The $element to assess.
413 * Returns the right format for the date element, or the original format
414 * if this is not a HTML5 element.
416 protected static function getHtml5DateFormat($element) {
417 switch ($element['#date_date_element']) {
419 return DateFormat::load('html_date')->getPattern();
422 case 'datetime-local':
423 return DateFormat::load('html_datetime')->getPattern();
426 return $element['#date_date_format'];
431 * Retrieves the right format for a HTML5 time element.
433 * The format is important because these elements will not work with any other
436 * @param string $element
437 * The $element to assess.
440 * Returns the right format for the time element, or the original format
441 * if this is not a HTML5 element.
443 protected static function getHtml5TimeFormat($element) {
444 switch ($element['#date_time_element']) {
446 return DateFormat::load('html_time')->getPattern();
449 return $element['#date_time_format'];