Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / lib / Drupal / Core / Datetime / Element / Datetime.php
1 <?php
2
3 namespace Drupal\Core\Datetime\Element;
4
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;
9
10 /**
11  * Provides a datetime element.
12  *
13  * @FormElement("datetime")
14  */
15 class Datetime extends DateElementBase {
16
17   /**
18    * @var \DateTimeInterface
19    */
20   protected static $dateExample;
21
22   /**
23    * {@inheritdoc}
24    */
25   public function getInfo() {
26     $date_format = '';
27     $time_format = '';
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();
33       }
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();
37       }
38     }
39
40     $class = get_class($this);
41     return [
42       '#input' => TRUE,
43       '#element_validate' => [
44         [$class, 'validateDatetime'],
45       ],
46       '#process' => [
47         [$class, 'processDatetime'],
48         [$class, 'processAjaxForm'],
49         [$class, 'processGroup'],
50       ],
51       '#pre_render' => [
52         [$class, 'preRenderGroup'],
53       ],
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' => '',
65     ];
66   }
67
68   /**
69    * {@inheritdoc}
70    */
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;
78
79       // Seconds will be omitted in a post in case there's no entry.
80       if (!empty($time_input) && strlen($time_input) == 5) {
81         $time_input .= ':00';
82       }
83
84       try {
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);
88       }
89       catch (\Exception $e) {
90         $date = NULL;
91       }
92       $input = [
93         'date'   => $date_input,
94         'time'   => $time_input,
95         'object' => $date,
96       ];
97     }
98     else {
99       $date = $element['#default_value'];
100       if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
101         $input = [
102           'date'   => $date->format($element['#date_date_format']),
103           'time'   => $date->format($element['#date_time_format']),
104           'object' => $date,
105         ];
106       }
107       else {
108         $input = [
109           'date'   => '',
110           'time'   => '',
111           'object' => NULL,
112         ];
113       }
114     }
115     return $input;
116   }
117
118   /**
119    * Expands a datetime element type into date and/or time elements.
120    *
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.
124    *
125    * Examples of possible configurations include:
126    *   HTML5 date and time:
127    *     #date_date_element = 'date';
128    *     #date_time_element = 'time';
129    *   HTML5 datetime:
130    *     #date_date_element = 'datetime';
131    *     #date_time_element = 'none';
132    *   HTML5 time only:
133    *     #date_date_element = 'none';
134    *     #date_time_element = 'time'
135    *   Non-HTML5:
136    *     #date_date_element = 'text';
137    *     #date_time_element = 'text';
138    *
139    * Required settings:
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
190    *     second.
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().
198    *
199    * Example usage:
200    * @code
201    *   $form = array(
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',
207    *   );
208    * @endcode
209    *
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.
216    *
217    * @return array
218    *   The form element whose value has been processed.
219    */
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;
224
225     // Set a fallback timezone.
226     if ($date instanceof DrupalDateTime) {
227       $element['#date_timezone'] = $date->getTimezone()->getName();
228     }
229     elseif (empty($element['#timezone'])) {
230       $element['#date_timezone'] = drupal_get_user_timezone();
231     }
232
233     $element['#tree'] = TRUE;
234
235     if ($element['#date_date_element'] != 'none') {
236
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'];
239
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'],
246       ];
247
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);
255
256         $extra_attributes += [
257           'min' => $html5_min->format($date_format, $format_settings),
258           'max' => $html5_max->format($date_format, $format_settings),
259         ];
260       }
261
262       $element['date'] = [
263         '#type' => 'date',
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'],
272       ];
273
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);
279           }
280         }
281       }
282     }
283
284     if ($element['#date_time_element'] != 'none') {
285
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'];
288
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'],
294       ];
295       $element['time'] = [
296         '#type' => 'date',
297         '#title' => t('Time'),
298         '#title_display' => 'invisible',
299         '#value' => $time_value,
300         '#attributes' => $element['#attributes'] + $extra_attributes,
301         '#required' => $element['#required'],
302         '#size' => 12,
303         '#error_no_message' => TRUE,
304       ];
305
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);
311           }
312         }
313       }
314     }
315
316     return $element;
317   }
318
319   /**
320    * {@inheritdoc}
321    */
322   public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) {
323     $element = parent::processAjaxForm($element, $form_state, $complete_form);
324
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'];
329       }
330       if (isset($element['time'])) {
331         $element['time']['#ajax'] = $element['#ajax'];
332       }
333     }
334
335     return $element;
336   }
337
338   /**
339    * Validation callback for a datetime element.
340    *
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.
345    *
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.
352    */
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);
356     if ($input_exists) {
357
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);
362
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);
366       }
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)]));
371       }
372       else {
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);
377         }
378         // If the date is invalid, set an error. A reminder of the required
379         // format in the message provides a good UX.
380         else {
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)]));
382         }
383       }
384     }
385   }
386
387   /**
388    * Creates an example for a date format.
389    *
390    * This is centralized for a consistent method of creating these examples.
391    *
392    * @param string $format
393    *
394    * @return string
395    */
396   public static function formatExample($format) {
397     if (!static::$dateExample) {
398       static::$dateExample = new DrupalDateTime();
399     }
400     return static::$dateExample->format($format);
401   }
402
403   /**
404    * Retrieves the right format for a HTML5 date element.
405    *
406    * The format is important because these elements will not work with any other
407    * format.
408    *
409    * @param string $element
410    *   The $element to assess.
411    *
412    * @return string
413    *   Returns the right format for the date element, or the original format
414    *   if this is not a HTML5 element.
415    */
416   protected static function getHtml5DateFormat($element) {
417     switch ($element['#date_date_element']) {
418       case 'date':
419         return DateFormat::load('html_date')->getPattern();
420
421       case 'datetime':
422       case 'datetime-local':
423         return DateFormat::load('html_datetime')->getPattern();
424
425       default:
426         return $element['#date_date_format'];
427     }
428   }
429
430   /**
431    * Retrieves the right format for a HTML5 time element.
432    *
433    * The format is important because these elements will not work with any other
434    * format.
435    *
436    * @param string $element
437    *   The $element to assess.
438    *
439    * @return string
440    *   Returns the right format for the time element, or the original format
441    *   if this is not a HTML5 element.
442    */
443   protected static function getHtml5TimeFormat($element) {
444     switch ($element['#date_time_element']) {
445       case 'time':
446         return DateFormat::load('html_time')->getPattern();
447
448       default:
449         return $element['#date_time_format'];
450     }
451   }
452
453 }