6f110d395f828f3a0b6a57b8215f142e954cc69f
[yaffs-website] / web / core / lib / Drupal / Core / Datetime / Element / Datelist.php
1 <?php
2
3 namespace Drupal\Core\Datetime\Element;
4
5 use Drupal\Component\Utility\NestedArray;
6 use Drupal\Core\Datetime\DateHelper;
7 use Drupal\Core\Datetime\DrupalDateTime;
8 use Drupal\Core\Form\FormStateInterface;
9
10 /**
11  * Provides a datelist element.
12  *
13  * @FormElement("datelist")
14  */
15 class Datelist extends DateElementBase {
16
17   /**
18    * {@inheritdoc}
19    */
20   public function getInfo() {
21     $class = get_class($this);
22     return [
23       '#input' => TRUE,
24       '#element_validate' => [
25         [$class, 'validateDatelist'],
26       ],
27       '#process' => [
28         [$class, 'processDatelist'],
29       ],
30       '#theme' => 'datetime_form',
31       '#theme_wrappers' => ['datetime_wrapper'],
32       '#date_part_order' => ['year', 'month', 'day', 'hour', 'minute'],
33       '#date_year_range' => '1900:2050',
34       '#date_increment' => 1,
35       '#date_date_callbacks' => [],
36       '#date_timezone' => '',
37     ];
38   }
39
40   /**
41    * {@inheritdoc}
42    *
43    * Validates the date type to adjust 12 hour time and prevent invalid dates.
44    * If the date is valid, the date is set in the form.
45    */
46   public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
47     $parts = $element['#date_part_order'];
48     $increment = $element['#date_increment'];
49
50     $date = NULL;
51     if ($input !== FALSE) {
52       $return = $input;
53       if (empty(static::checkEmptyInputs($input, $parts))) {
54         if (isset($input['ampm'])) {
55           if ($input['ampm'] == 'pm' && $input['hour'] < 12) {
56             $input['hour'] += 12;
57           }
58           elseif ($input['ampm'] == 'am' && $input['hour'] == 12) {
59             $input['hour'] -= 12;
60           }
61           unset($input['ampm']);
62         }
63         $timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
64         try {
65           $date = DrupalDateTime::createFromArray($input, $timezone);
66         }
67         catch (\Exception $e) {
68           $form_state->setError($element, t('Selected combination of day and month is not valid.'));
69         }
70         if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
71           static::incrementRound($date, $increment);
72         }
73       }
74     }
75     else {
76       $return = array_fill_keys($parts, '');
77       if (!empty($element['#default_value'])) {
78         $date = $element['#default_value'];
79         if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
80           static::incrementRound($date, $increment);
81           foreach ($parts as $part) {
82             switch ($part) {
83               case 'day':
84                 $format = 'j';
85                 break;
86
87               case 'month':
88                 $format = 'n';
89                 break;
90
91               case 'year':
92                 $format = 'Y';
93                 break;
94
95               case 'hour':
96                 $format = in_array('ampm', $element['#date_part_order']) ? 'g' : 'G';
97                 break;
98
99               case 'minute':
100                 $format = 'i';
101                 break;
102
103               case 'second':
104                 $format = 's';
105                 break;
106
107               case 'ampm':
108                 $format = 'a';
109                 break;
110
111               default:
112                 $format = '';
113
114             }
115             $return[$part] = $date->format($format);
116           }
117         }
118       }
119     }
120     $return['object'] = $date;
121     return $return;
122   }
123
124   /**
125    * Expands a date element into an array of individual elements.
126    *
127    * Required settings:
128    *   - #default_value: A DrupalDateTime object, adjusted to the proper local
129    *     timezone. Converting a date stored in the database from UTC to the local
130    *     zone and converting it back to UTC before storing it is not handled here.
131    *     This element accepts a date as the default value, and then converts the
132    *     user input strings back into a new date object on submission. No timezone
133    *     adjustment is performed.
134    * Optional properties include:
135    *   - #date_part_order: Array of date parts indicating the parts and order
136    *     that should be used in the selector, optionally including 'ampm' for
137    *     12 hour time. Default is array('year', 'month', 'day', 'hour', 'minute').
138    *   - #date_text_parts: Array of date parts that should be presented as
139    *     text fields instead of drop-down selectors. Default is an empty array.
140    *   - #date_date_callbacks: Array of optional callbacks for the date element.
141    *   - #date_year_range: A description of the range of years to allow, like
142    *     '1900:2050', '-3:+3' or '2000:+3', where the first value describes the
143    *     earliest year and the second the latest year in the range. A year
144    *     in either position means that specific year. A +/- value describes a
145    *     dynamic value that is that many years earlier or later than the current
146    *     year at the time the form is displayed. Defaults to '1900:2050'.
147    *   - #date_increment: The increment to use for minutes and seconds, i.e.
148    *     '15' would show only :00, :15, :30 and :45. Defaults to 1 to show every
149    *     minute.
150    *   - #date_timezone: The local timezone to use when creating dates. Generally
151    *     this should be left empty and it will be set correctly for the user using
152    *     the form. Useful if the default value is empty to designate a desired
153    *     timezone for dates created in form processing. If a default date is
154    *     provided, this value will be ignored, the timezone in the default date
155    *     takes precedence. Defaults to the value returned by
156    *     drupal_get_user_timezone().
157    *
158    * Example usage:
159    * @code
160    *   $form = array(
161    *     '#type' => 'datelist',
162    *     '#default_value' => new DrupalDateTime('2000-01-01 00:00:00'),
163    *     '#date_part_order' => array('month', 'day', 'year', 'hour', 'minute', 'ampm'),
164    *     '#date_text_parts' => array('year'),
165    *     '#date_year_range' => '2010:2020',
166    *     '#date_increment' => 15,
167    *   );
168    * @endcode
169    *
170    * @param array $element
171    *   The form element whose value is being processed.
172    * @param \Drupal\Core\Form\FormStateInterface $form_state
173    *   The current state of the form.
174    * @param array $complete_form
175    *   The complete form structure.
176    *
177    * @return array
178    */
179   public static function processDatelist(&$element, FormStateInterface $form_state, &$complete_form) {
180     // Load translated date part labels from the appropriate calendar plugin.
181     $date_helper = new DateHelper();
182
183     // The value callback has populated the #value array.
184     $date = !empty($element['#value']['object']) ? $element['#value']['object'] : NULL;
185
186     // Set a fallback timezone.
187     if ($date instanceof DrupalDateTime) {
188       $element['#date_timezone'] = $date->getTimezone()->getName();
189     }
190     elseif (!empty($element['#timezone'])) {
191       $element['#date_timezone'] = $element['#date_timezone'];
192     }
193     else {
194       $element['#date_timezone'] = drupal_get_user_timezone();
195     }
196
197     $element['#tree'] = TRUE;
198
199     // Determine the order of the date elements.
200     $order = !empty($element['#date_part_order']) ? $element['#date_part_order'] : ['year', 'month', 'day'];
201     $text_parts = !empty($element['#date_text_parts']) ? $element['#date_text_parts'] : [];
202
203     // Output multi-selector for date.
204     foreach ($order as $part) {
205       switch ($part) {
206         case 'day':
207           $options = $date_helper->days($element['#required']);
208           $format = 'j';
209           $title = t('Day');
210           break;
211
212         case 'month':
213           $options = $date_helper->monthNamesAbbr($element['#required']);
214           $format = 'n';
215           $title = t('Month');
216           break;
217
218         case 'year':
219           $range = static::datetimeRangeYears($element['#date_year_range'], $date);
220           $options = $date_helper->years($range[0], $range[1], $element['#required']);
221           $format = 'Y';
222           $title = t('Year');
223           break;
224
225         case 'hour':
226           $format = in_array('ampm', $element['#date_part_order']) ? 'g' : 'G';
227           $options = $date_helper->hours($format, $element['#required']);
228           $title = t('Hour');
229           break;
230
231         case 'minute':
232           $format = 'i';
233           $options = $date_helper->minutes($format, $element['#required'], $element['#date_increment']);
234           $title = t('Minute');
235           break;
236
237         case 'second':
238           $format = 's';
239           $options = $date_helper->seconds($format, $element['#required'], $element['#date_increment']);
240           $title = t('Second');
241           break;
242
243         case 'ampm':
244           $format = 'a';
245           $options = $date_helper->ampm($element['#required']);
246           $title = t('AM/PM');
247           break;
248
249         default:
250           $format = '';
251           $options = [];
252           $title = '';
253       }
254
255       $default = isset($element['#value'][$part]) && trim($element['#value'][$part]) != '' ? $element['#value'][$part] : '';
256       $value = $date instanceof DrupalDateTime && !$date->hasErrors() ? $date->format($format) : $default;
257       if (!empty($value) && $part != 'ampm') {
258         $value = intval($value);
259       }
260
261       $element['#attributes']['title'] = $title;
262       $element[$part] = [
263         '#type' => in_array($part, $text_parts) ? 'textfield' : 'select',
264         '#title' => $title,
265         '#title_display' => 'invisible',
266         '#value' => $value,
267         '#attributes' => $element['#attributes'],
268         '#options' => $options,
269         '#required' => $element['#required'],
270         '#error_no_message' => FALSE,
271         '#empty_option' => $title,
272       ];
273     }
274
275     // Allows custom callbacks to alter the element.
276     if (!empty($element['#date_date_callbacks'])) {
277       foreach ($element['#date_date_callbacks'] as $callback) {
278         if (function_exists($callback)) {
279           $callback($element, $form_state, $date);
280         }
281       }
282     }
283
284     return $element;
285   }
286
287   /**
288    * Validation callback for a datelist element.
289    *
290    * If the date is valid, the date object created from the user input is set in
291    * the form for use by the caller. The work of compiling the user input back
292    * into a date object is handled by the value callback, so we can use it here.
293    * We also have the raw input available for validation testing.
294    *
295    * @param array $element
296    *   The element being processed.
297    * @param \Drupal\Core\Form\FormStateInterface $form_state
298    *   The current state of the form.
299    * @param array $complete_form
300    *   The complete form structure.
301    */
302   public static function validateDatelist(&$element, FormStateInterface $form_state, &$complete_form) {
303     $input_exists = FALSE;
304     $input = NestedArray::getValue($form_state->getValues(), $element['#parents'], $input_exists);
305     $title = static::getElementTitle($element, $complete_form);
306
307     if ($input_exists) {
308       $all_empty = static::checkEmptyInputs($input, $element['#date_part_order']);
309
310       // If there's empty input and the field is not required, set it to empty.
311       if (empty($input['year']) && empty($input['month']) && empty($input['day']) && !$element['#required']) {
312         $form_state->setValueForElement($element, NULL);
313       }
314       // If there's empty input and the field is required, set an error.
315       elseif (empty($input['year']) && empty($input['month']) && empty($input['day']) && $element['#required']) {
316         $form_state->setError($element, t('The %field date is required.', ['%field' => $title]));
317       }
318       elseif (!empty($all_empty)) {
319         foreach ($all_empty as $value) {
320           $form_state->setError($element, t('The %field date is incomplete.', ['%field' => $title]));
321           $form_state->setError($element[$value], t('A value must be selected for %part.', ['%part' => $value]));
322         }
323       }
324       else {
325         // If the input is valid, set it.
326         $date = $input['object'];
327         if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
328           $form_state->setValueForElement($element, $date);
329         }
330         // If the input is invalid and an error doesn't exist, set one.
331         elseif ($form_state->getError($element) === NULL) {
332           $form_state->setError($element, t('The %field date is invalid.', ['%field' => $title]));
333         }
334       }
335     }
336   }
337
338   /**
339    * Checks the input array for empty values.
340    *
341    * Input array keys are checked against values in the parts array. Elements
342    * not in the parts array are ignored. Returns an array representing elements
343    * from the input array that have no value. If no empty values are found,
344    * returned array is empty.
345    *
346    * @param array $input
347    *   Array of individual inputs to check for value.
348    * @param array $parts
349    *   Array to check input against, ignoring elements not in this array.
350    *
351    * @return array
352    *   Array of keys from the input array that have no value, may be empty.
353    */
354   protected static function checkEmptyInputs($input, $parts) {
355     // Filters out empty array values, any valid value would have a string length.
356     $filtered_input = array_filter($input, 'strlen');
357     return array_diff($parts, array_keys($filtered_input));
358   }
359
360   /**
361    * Rounds minutes and seconds to nearest requested value.
362    *
363    * @param $date
364    * @param $increment
365    *
366    * @return
367    */
368   protected static function incrementRound(&$date, $increment) {
369     // Round minutes and seconds, if necessary.
370     if ($date instanceof DrupalDateTime && $increment > 1) {
371       $day = intval($date->format('j'));
372       $hour = intval($date->format('H'));
373       $second = intval(round(intval($date->format('s')) / $increment) * $increment);
374       $minute = intval($date->format('i'));
375       if ($second == 60) {
376         $minute += 1;
377         $second = 0;
378       }
379       $minute = intval(round($minute / $increment) * $increment);
380       if ($minute == 60) {
381         $hour += 1;
382         $minute = 0;
383       }
384       $date->setTime($hour, $minute, $second);
385       if ($hour == 24) {
386         $day += 1;
387         $year = $date->format('Y');
388         $month = $date->format('n');
389         $date->setDate($year, $month, $day);
390       }
391     }
392     return $date;
393   }
394
395 }