3 namespace Drupal\Core\Datetime\Element;
5 use Drupal\Component\Utility\NestedArray;
6 use Drupal\Core\Datetime\DateHelper;
7 use Drupal\Core\Datetime\DrupalDateTime;
8 use Drupal\Core\Form\FormStateInterface;
11 * Provides a datelist element.
13 * @FormElement("datelist")
15 class Datelist extends DateElementBase {
20 public function getInfo() {
21 $class = get_class($this);
24 '#element_validate' => [
25 [$class, 'validateDatelist'],
28 [$class, 'processDatelist'],
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' => '',
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.
46 public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
47 $parts = $element['#date_part_order'];
48 $increment = $element['#date_increment'];
51 if ($input !== FALSE) {
53 if (empty(static::checkEmptyInputs($input, $parts))) {
54 if (isset($input['ampm'])) {
55 if ($input['ampm'] == 'pm' && $input['hour'] < 12) {
58 elseif ($input['ampm'] == 'am' && $input['hour'] == 12) {
61 unset($input['ampm']);
63 $timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
65 $date = DrupalDateTime::createFromArray($input, $timezone);
67 catch (\Exception $e) {
68 $form_state->setError($element, t('Selected combination of day and month is not valid.'));
70 if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
71 static::incrementRound($date, $increment);
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) {
96 $format = in_array('ampm', $element['#date_part_order']) ? 'g' : 'G';
115 $return[$part] = $date->format($format);
120 $return['object'] = $date;
125 * Expands a date element into an array of individual elements.
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
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().
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,
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.
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();
183 // The value callback has populated the #value array.
184 $date = !empty($element['#value']['object']) ? $element['#value']['object'] : NULL;
186 // Set a fallback timezone.
187 if ($date instanceof DrupalDateTime) {
188 $element['#date_timezone'] = $date->getTimezone()->getName();
190 elseif (!empty($element['#timezone'])) {
191 $element['#date_timezone'] = $element['#date_timezone'];
194 $element['#date_timezone'] = drupal_get_user_timezone();
197 $element['#tree'] = TRUE;
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'] : [];
203 // Output multi-selector for date.
204 foreach ($order as $part) {
207 $options = $date_helper->days($element['#required']);
213 $options = $date_helper->monthNamesAbbr($element['#required']);
219 $range = static::datetimeRangeYears($element['#date_year_range'], $date);
220 $options = $date_helper->years($range[0], $range[1], $element['#required']);
226 $format = in_array('ampm', $element['#date_part_order']) ? 'g' : 'G';
227 $options = $date_helper->hours($format, $element['#required']);
233 $options = $date_helper->minutes($format, $element['#required'], $element['#date_increment']);
234 $title = t('Minute');
239 $options = $date_helper->seconds($format, $element['#required'], $element['#date_increment']);
240 $title = t('Second');
245 $options = $date_helper->ampm($element['#required']);
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);
261 $element['#attributes']['title'] = $title;
263 '#type' => in_array($part, $text_parts) ? 'textfield' : 'select',
265 '#title_display' => 'invisible',
267 '#attributes' => $element['#attributes'],
268 '#options' => $options,
269 '#required' => $element['#required'],
270 '#error_no_message' => FALSE,
271 '#empty_option' => $title,
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);
288 * Validation callback for a datelist element.
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.
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.
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);
308 $all_empty = static::checkEmptyInputs($input, $element['#date_part_order']);
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);
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]));
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]));
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);
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]));
339 * Checks the input array for empty values.
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.
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.
352 * Array of keys from the input array that have no value, may be empty.
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));
361 * Rounds minutes and seconds to nearest requested value.
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'));
379 $minute = intval(round($minute / $increment) * $increment);
384 $date->setTime($hour, $minute, $second);
387 $year = $date->format('Y');
388 $month = $date->format('n');
389 $date->setDate($year, $month, $day);