class Datelist

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

Provides a datelist element.

Hierarchy

Expanded class hierarchy of Datelist

2 string references to 'Datelist'
TimezoneTest::testDatelistElementTimesUnderstoodCorrectly in core/tests/Drupal/KernelTests/Core/Datetime/Element/TimezoneTest.php
Tests datelist elements interpret their times correctly when saving.
TimezoneTest::testDatelistTimezonePropertyProcessed in core/tests/Drupal/KernelTests/Core/Datetime/Element/TimezoneTest.php
On datelist elements test #date_timezone after ::processDatetime.

File

core/lib/Drupal/Core/Datetime/Element/Datelist.php, line 18

Namespace

Drupal\Core\Datetime\Element
View source
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;
    }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title Overrides
DateElementBase::datetimeRangeYears protected static function Specifies the start and end year to use as a date range.
DateElementBase::getElementTitle protected static function Returns the most relevant title of a datetime element.
Datelist::checkEmptyInputs protected static function Checks the input array for empty values.
Datelist::getInfo public function Returns the element properties for this element. Overrides ElementInterface::getInfo
Datelist::incrementRound protected static function Rounds minutes and seconds to nearest requested value.
Datelist::processDatelist public static function Expands a date element into an array of individual elements.
Datelist::validateDatelist public static function Validation callback for a datelist element.
Datelist::valueCallback public static function 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.
Overrides FormElementBase::valueCallback
DoTrustedCallbackTrait::doTrustedCallback public function Performs a callback.
FormElementBase::processAutocomplete public static function Adds autocomplete functionality to elements. 1
FormElementBase::processPattern public static function #process callback for #pattern form element property. 1
FormElementBase::validatePattern public static function #element_validate callback for #pattern form element property. 1
PluginInspectionInterface::getPluginDefinition public function Gets the definition of the plugin implementation. 6
PluginInspectionInterface::getPluginId public function Gets the plugin ID of the plugin instance. 2
RenderElementBase::preRenderAjaxForm public static function Adds Ajax information about an element to communicate with JavaScript. 2
RenderElementBase::preRenderGroup public static function Adds members of this group as actual elements for rendering. 2
RenderElementBase::processAjaxForm public static function Form element processing handler for the #ajax form property. 3
RenderElementBase::processGroup public static function Arranges elements into groups. 2
RenderElementBase::setAttributes public static function Overrides ElementInterface::setAttributes 2

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