Datelist.php

Same filename in other branches
  1. 9 core/lib/Drupal/Core/Datetime/Element/Datelist.php
  2. 8.9.x core/lib/Drupal/Core/Datetime/Element/Datelist.php
  3. 10 core/lib/Drupal/Core/Datetime/Element/Datelist.php

Namespace

Drupal\Core\Datetime\Element

File

core/lib/Drupal/Core/Datetime/Element/Datelist.php

View source
<?php

namespace Drupal\Core\Datetime\Element;

use Drupal\Component\Utility\FilterArray;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Variable;
use Drupal\Core\Datetime\DateHelper;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Attribute\FormElement;
use Drupal\Core\Security\DoTrustedCallbackTrait;
use Drupal\Core\Security\StaticTrustedCallbackHelper;

/**
 * Provides a datelist element.
 */
class Datelist extends DateElementBase {
    use DoTrustedCallbackTrait;
    
    /**
     * {@inheritdoc}
     */
    public function getInfo() {
        $class = static::class;
        return [
            '#input' => TRUE,
            '#element_validate' => [
                [
                    $class,
                    'validateDatelist',
                ],
            ],
            '#process' => [
                [
                    $class,
                    'processDatelist',
                ],
            ],
            '#theme' => 'datetime_form',
            '#theme_wrappers' => [
                'datetime_wrapper',
            ],
            '#date_part_order' => [
                'year',
                'month',
                'day',
                'hour',
                'minute',
            ],
            '#date_year_range' => '1900:2050',
            '#date_increment' => 1,
            '#date_date_callbacks' => [],
            '#date_timezone' => date_default_timezone_get(),
        ];
    }
    
    /**
     * {@inheritdoc}
     *
     * Validates the date type to adjust 12 hour time and prevent invalid dates.
     * If the date is valid, the date is set in the form.
     */
    public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
        $parts = $element['#date_part_order'];
        $increment = $element['#date_increment'];
        $date = NULL;
        if ($input !== FALSE) {
            $return = $input;
            if (empty(static::checkEmptyInputs($input, $parts))) {
                if (isset($input['ampm'])) {
                    if ($input['ampm'] == 'pm' && $input['hour'] < 12) {
                        $input['hour'] += 12;
                    }
                    elseif ($input['ampm'] == 'am' && $input['hour'] == 12) {
                        $input['hour'] -= 12;
                    }
                    unset($input['ampm']);
                }
                try {
                    $date = DrupalDateTime::createFromArray($input, $element['#date_timezone']);
                } catch (\Exception) {
                    $form_state->setError($element, t('Selected combination of day and month is not valid.'));
                }
                if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
                    static::incrementRound($date, $increment);
                }
            }
        }
        else {
            $return = array_fill_keys($parts, '');
            if (!empty($element['#default_value'])) {
                $date = $element['#default_value'];
                if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
                    $date->setTimezone(new \DateTimeZone($element['#date_timezone']));
                    static::incrementRound($date, $increment);
                    foreach ($parts as $part) {
                        switch ($part) {
                            case 'day':
                                $format = 'j';
                                break;
                            case 'month':
                                $format = 'n';
                                break;
                            case 'year':
                                $format = 'Y';
                                break;
                            case 'hour':
                                $format = in_array('ampm', $element['#date_part_order']) ? 'g' : 'G';
                                break;
                            case 'minute':
                                $format = 'i';
                                break;
                            case 'second':
                                $format = 's';
                                break;
                            case 'ampm':
                                $format = 'a';
                                break;
                            default:
                                $format = '';
                        }
                        $return[$part] = $date->format($format);
                    }
                }
            }
        }
        $return['object'] = $date;
        return $return;
    }
    
    /**
     * Expands a date element into an array of individual elements.
     *
     * Required settings:
     *   - #default_value: A DrupalDateTime object, adjusted to the proper local
     *     timezone. Converting a date stored in the database from UTC to the local
     *     zone and converting it back to UTC before storing it is not handled here.
     *     This element accepts a date as the default value, and then converts the
     *     user input strings back into a new date object on submission. No timezone
     *     adjustment is performed.
     * Optional properties include:
     *   - #date_part_order: Array of date parts indicating the parts and order
     *     that should be used in the selector, optionally including 'ampm' for
     *     12 hour time. Default is ['year', 'month', 'day', 'hour', 'minute'].
     *   - #date_text_parts: Array of date parts that should be presented as
     *     text fields instead of drop-down selectors. Default is an empty array.
     *   - #date_date_callbacks: Array of optional callbacks for the date element.
     *   - #date_year_range: A description of the range of years to allow, like
     *     '1900:2050', '-3:+3' or '2000:+3', where the first value describes the
     *     earliest year and the second the latest year in the range. A year
     *     in either position means that specific year. A +/- value describes a
     *     dynamic value that is that many years earlier or later than the current
     *     year at the time the form is displayed. Defaults to '1900:2050'.
     *   - #date_increment: The increment to use for minutes and seconds, i.e.
     *     '15' would show only :00, :15, :30 and :45. Defaults to 1 to show every
     *     minute.
     *   - #date_timezone: The Time Zone Identifier (TZID) to use when displaying
     *     or interpreting dates, i.e: 'Asia/Kolkata'. Defaults to the value
     *     returned by date_default_timezone_get().
     *
     * Example usage:
     * @code
     *   $form = [
     *     '#type' => 'datelist',
     *     '#default_value' => new DrupalDateTime('2000-01-01 00:00:00'),
     *     '#date_part_order' => ['month', 'day', 'year', 'hour', 'minute', 'ampm'],
     *     '#date_text_parts' => ['year'],
     *     '#date_year_range' => '2010:2020',
     *     '#date_increment' => 15,
     *     '#date_timezone' => 'Asia/Kolkata'
     *   ];
     * @endcode
     *
     * @param array $element
     *   The form element whose value is being processed.
     * @param \Drupal\Core\Form\FormStateInterface $form_state
     *   The current state of the form.
     * @param array $complete_form
     *   The complete form structure.
     *
     * @return array
     */
    public static function processDatelist(&$element, FormStateInterface $form_state, &$complete_form) {
        // Load translated date part labels from the appropriate calendar plugin.
        $date_helper = new DateHelper();
        // The value callback has populated the #value array.
        $date = !empty($element['#value']['object']) ? $element['#value']['object'] : NULL;
        $element['#tree'] = TRUE;
        // Determine the order of the date elements.
        $order = !empty($element['#date_part_order']) ? $element['#date_part_order'] : [
            'year',
            'month',
            'day',
        ];
        $text_parts = !empty($element['#date_text_parts']) ? $element['#date_text_parts'] : [];
        // Output multi-selector for date.
        foreach ($order as $part) {
            switch ($part) {
                case 'day':
                    $options = $date_helper->days($element['#required']);
                    $format = 'j';
                    $title = t('Day');
                    break;
                case 'month':
                    $options = $date_helper->monthNamesAbbr($element['#required']);
                    $format = 'n';
                    $title = t('Month');
                    break;
                case 'year':
                    $range = static::datetimeRangeYears($element['#date_year_range'], $date);
                    $options = $date_helper->years($range[0], $range[1], $element['#required']);
                    $format = 'Y';
                    $title = t('Year');
                    break;
                case 'hour':
                    $format = in_array('ampm', $element['#date_part_order']) ? 'g' : 'G';
                    $options = $date_helper->hours($format, $element['#required']);
                    $title = t('Hour');
                    break;
                case 'minute':
                    $format = 'i';
                    $options = $date_helper->minutes($format, $element['#required'], $element['#date_increment']);
                    $title = t('Minute');
                    break;
                case 'second':
                    $format = 's';
                    $options = $date_helper->seconds($format, $element['#required'], $element['#date_increment']);
                    $title = t('Second');
                    break;
                case 'ampm':
                    $format = 'a';
                    $options = $date_helper->ampm($element['#required']);
                    $title = t('AM/PM');
                    break;
                default:
                    $format = '';
                    $options = [];
                    $title = '';
            }
            $default = isset($element['#value'][$part]) && trim($element['#value'][$part]) != '' ? $element['#value'][$part] : '';
            $value = $date instanceof DrupalDateTime && !$date->hasErrors() ? $date->format($format) : $default;
            if (!empty($value) && $part != 'ampm') {
                $value = intval($value);
            }
            $element['#attributes']['title'] = $title;
            $element[$part] = [
                '#type' => in_array($part, $text_parts) ? 'textfield' : 'select',
                '#title' => $title,
                '#value' => $value,
                '#attributes' => $element['#attributes'],
                '#options' => $options,
                '#required' => $element['#required'],
                '#error_no_message' => FALSE,
                '#empty_option' => $title,
            ];
        }
        // Allows custom callbacks to alter the element.
        if (!empty($element['#date_date_callbacks'])) {
            foreach ($element['#date_date_callbacks'] as $callback) {
                $message = sprintf('Datelist element #date_date_callbacks callbacks must be methods of a class that implements \\Drupal\\Core\\Security\\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/3217966', Variable::callableToString($callback));
                StaticTrustedCallbackHelper::callback($callback, [
                    &$element,
                    $form_state,
                    $date,
                ], $message);
            }
        }
        return $element;
    }
    
    /**
     * Validation callback for a datelist element.
     *
     * If the date is valid, the date object created from the user input is set in
     * the form for use by the caller. The work of compiling the user input back
     * into a date object is handled by the value callback, so we can use it here.
     * We also have the raw input available for validation testing.
     *
     * @param array $element
     *   The element being processed.
     * @param \Drupal\Core\Form\FormStateInterface $form_state
     *   The current state of the form.
     * @param array $complete_form
     *   The complete form structure.
     */
    public static function validateDatelist(&$element, FormStateInterface $form_state, &$complete_form) {
        $input_exists = FALSE;
        $input = NestedArray::getValue($form_state->getValues(), $element['#parents'], $input_exists);
        $title = static::getElementTitle($element, $complete_form);
        if ($input_exists) {
            $all_empty = static::checkEmptyInputs($input, $element['#date_part_order']);
            // If there's empty input and the field is not required, set it to empty.
            if (empty($input['year']) && empty($input['month']) && empty($input['day']) && !$element['#required']) {
                $form_state->setValueForElement($element, NULL);
            }
            elseif (empty($input['year']) && empty($input['month']) && empty($input['day']) && $element['#required']) {
                $form_state->setError($element, t('The %field date is required.', [
                    '%field' => $title,
                ]));
            }
            elseif (!empty($all_empty)) {
                foreach ($all_empty as $value) {
                    $form_state->setError($element, t('The %field date is incomplete.', [
                        '%field' => $title,
                    ]));
                    $form_state->setError($element[$value], t('A value must be selected for %part.', [
                        '%part' => $value,
                    ]));
                }
            }
            else {
                // If the input is valid, set it.
                $date = $input['object'];
                if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
                    $form_state->setValueForElement($element, $date);
                }
                elseif ($form_state->getError($element) === NULL) {
                    $form_state->setError($element, t('The %field date is invalid.', [
                        '%field' => $title,
                    ]));
                }
            }
        }
    }
    
    /**
     * Checks the input array for empty values.
     *
     * Input array keys are checked against values in the parts array. Elements
     * not in the parts array are ignored. Returns an array representing elements
     * from the input array that have no value. If no empty values are found,
     * returned array is empty.
     *
     * @param array $input
     *   Array of individual inputs to check for value.
     * @param array $parts
     *   Array to check input against, ignoring elements not in this array.
     *
     * @return array
     *   Array of keys from the input array that have no value, may be empty.
     */
    protected static function checkEmptyInputs($input, $parts) {
        // The object key does not represent an input value, see
        // \Drupal\Core\Datetime\Element\Datelist::valueCallback().
        unset($input['object']);
        // Filters out empty array values, any valid value would have a string length.
        $filtered_input = FilterArray::removeEmptyStrings($input);
        return array_diff($parts, array_keys($filtered_input));
    }
    
    /**
     * Rounds minutes and seconds to nearest requested value.
     *
     * @param $date
     *   The date.
     * @param $increment
     *   The value to round to.
     *
     * @return \Drupal\Core\Datetime\DrupalDateTime
     */
    protected static function incrementRound(&$date, $increment) {
        // Round minutes and seconds, if necessary.
        if ($date instanceof DrupalDateTime && $increment > 1) {
            $day = intval($date->format('j'));
            $hour = intval($date->format('H'));
            $second = intval(round(intval($date->format('s')) / $increment) * $increment);
            $minute = intval($date->format('i'));
            if ($second == 60) {
                $minute += 1;
                $second = 0;
            }
            $minute = intval(round($minute / $increment) * $increment);
            if ($minute == 60) {
                $hour += 1;
                $minute = 0;
            }
            $date->setTime($hour, $minute, $second);
            if ($hour == 24) {
                $day += 1;
                $year = $date->format('Y');
                $month = $date->format('n');
                $date->setDate($year, $month, $day);
            }
        }
        return $date;
    }

}

Classes

Title Deprecated Summary
Datelist Provides a datelist element.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.