Skip to content
datetime.module 36.9 KiB
Newer Older
<?php

/**
 * @file
 * Field hooks to implement a simple datetime field.
 */

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Template\Attribute;
use Drupal\datetime\DateHelper;

/**
 * Defines the timezone that dates should be stored in.
 */
const DATETIME_STORAGE_TIMEZONE = 'UTC';

/**
 * Defines the format that date and time should be stored in.
 */
const DATETIME_DATETIME_STORAGE_FORMAT = 'Y-m-d\TH:i:s';

/**
 * Defines the format that dates should be stored in.
 */
const DATETIME_DATE_STORAGE_FORMAT = 'Y-m-d';

/**
 * Implements hook_help().
 */
function datetime_help($path, $arg) {
  switch ($path) {
    case 'help.page.datetime':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('The Datetime module provides a Date field that stores dates and times. It also provides the Form API elements <em>datetime</em> and <em>datelist</em> for use in programming modules. See the <a href="!field">Field module help</a> and the <a href="!field_ui">Field UI module help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href="!datetime_do">online documentation for the Datetime module</a>.', array('!field' => \Drupal::url('help.page', array('name' => 'field')), '!field_ui' => \Drupal::url('help.page', array('name' => 'field_ui')), '!datetime_do' => 'https://drupal.org/documentation/modules/datetime')) . '</p>';
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Managing and displaying date fields') . '</dt>';
      $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the Date field can be configured separately. See the <a href="!field_ui">Field UI help</a> for more information on how to manage fields and their display.', array('!field_ui' => \Drupal::url('help.page', array('name' => 'field_ui')))) . '</dd>';
      $output .= '<dt>' . t('Displaying dates') . '</dt>';
      $output .= '<dd>' . t('Dates can be displayed using the <em>Plain</em> or the <em>Default</em> formatter. The <em>Plain</em> formatter displays the date in the <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a> format. If you choose the <em>Default</em> formatter, you can choose a format from a predefined list that can be managed on the <a href="!date_format_list">Date and time formats</a> page.', array('!date_format_list'=> \Drupal::url('system.date_format_list'))) . '</dd>';
      $output .= '</dl>';
      return $output;
    }
}

/**
 * Implements hook_element_info().
 */
function datetime_element_info() {
  $date_format = '';
  $time_format = '';
  // Date formats cannot be loaded during install or update.
  if (!defined('MAINTENANCE_MODE')) {
    if ($date_format_entity = entity_load('date_format', 'html_date')) {
      $date_format = $date_format_entity->getPattern();
    }
    if ($time_format_entity = entity_load('date_format', 'html_time')) {
      $time_format = $time_format_entity->getPattern();
  $types['datetime'] = array(
    '#input' => TRUE,
    '#element_validate' => array('datetime_datetime_validate'),
    '#process' => array('datetime_datetime_form_process', 'form_process_group'),
    '#pre_render' => array('form_pre_render_group'),
    '#theme' => 'datetime_form',
    '#theme_wrappers' => array('datetime_wrapper'),
    '#date_date_element' => 'date',
    '#date_date_callbacks' => array(),
    '#date_time_element' => 'time',
    '#date_time_callbacks' => array(),
    '#date_year_range' => '1900:2050',
    '#date_increment' => 1,
    '#date_timezone' => '',
  );
  $types['datelist'] = array(
    '#input' => TRUE,
    '#element_validate' => array('datetime_datelist_validate'),
    '#process' => array('datetime_datelist_form_process'),
    '#theme_wrappers' => array('datetime_wrapper'),
    '#date_part_order' => array('year', 'month', 'day', 'hour', 'minute'),
    '#date_year_range' => '1900:2050',
    '#date_increment' => 1,
    '#date_date_callbacks' => array(),
    '#date_timezone' => '',
  );
  return $types;
}

/**
 * Implements hook_theme().
 */
function datetime_theme() {
  return array(
    'datetime_form' => array(
      'render element' => 'element',
    ),
    'datetime_wrapper' => array(
      'render element' => 'element',
    ),
  );
}

/**
 * Validation callback for the datetime widget element.
 *
 * The date has already been validated by the datetime form type validator and
 * transformed to an date object. We just need to convert the date back to a the
 * storage timezone and format.
 *
 * @param array $element
 *   The form element whose value is being validated.
 * @param array $form_state
 *   The current state of the form.
 */
function datetime_datetime_widget_validate(&$element, &$form_state) {
    $input_exists = FALSE;
    $input = NestedArray::getValue($form_state['values'], $element['#parents'], $input_exists);
    if ($input_exists) {
      // The date should have been returned to a date object at this point by
      // datetime_validate(), which runs before this.
      if (!empty($input['value'])) {
        $date = $input['value'];
        if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {

          // If this is a date-only field, set it to the default time so the
          // timezone conversion can be reversed.
          if ($element['value']['#date_time_element'] == 'none') {
            datetime_date_default_time($date);
          }
          // Adjust the date for storage.
          $date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
          $value = $date->format($element['value']['#date_storage_format']);
          form_set_value($element['value'], $value, $form_state);
        }
      }
    }
  }
}

/**
 * Validation callback for the datelist widget element.
 *
 * The date has already been validated by the datetime form type validator and
 * transformed to an date object. We just need to convert the date back to a the
 * storage timezone and format.
 *
 * @param array $element
 *   The form element whose value is being validated.
 * @param array $form_state
 *   The current state of the form.
 */
function datetime_datelist_widget_validate(&$element, &$form_state) {
    $input_exists = FALSE;
    $input = NestedArray::getValue($form_state['values'], $element['#parents'], $input_exists);
    if ($input_exists) {
      // The date should have been returned to a date object at this point by
      // datetime_validate(), which runs before this.
      if (!empty($input['value'])) {
        $date = $input['value'];
        if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {

          // If this is a date-only field, set it to the default time so the
          // timezone conversion can be reversed.
          if (!in_array('hour', $element['value']['#date_part_order'])) {
            datetime_date_default_time($date);
          }
          // Adjust the date for storage.
          $date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
          $value = $date->format($element['value']['#date_storage_format']);
          form_set_value($element['value'], $value, $form_state);
        }
      }
    }
  }
}

/**
 * Sets a consistent time on a date without time.
 *
 * The default time for a date without time can be anything, so long as it is
 * consistently applied. If we use noon, dates in most timezones will have the
 * same value for in both the local timezone and UTC.
 *
 * @param $date
 *
 */
function datetime_date_default_time($date) {
  $date->setTime(12, 0, 0);
}

/**
 * Prepares variables for datetime form element templates.
 * The datetime form element serves as a wrapper around the date element type,
 * which creates a date and a time component for a date.
 *
 * Default template: datetime-form.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: An associative array containing the properties of the element.
 *     Properties used: #title, #value, #options, #description, #required,
 *     #attributes.
 *
 * @see form_process_datetime()
 */
function template_preprocess_datetime_form(&$variables) {
  $variables['attributes'] = array();
    $variables['attributes']['id'] = $element['#id'];
  }
  if (!empty($element['#attributes']['class'])) {
    $variables['attributes']['class'] = (array) $element['#attributes']['class'];
  $variables['attributes']['class'][] = 'container-inline';
 * Prepares variables for datetime form wrapper templates.
 *
 * Default template: datetime-wrapper.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: An associative array containing the properties of the element.
 *     Properties used: #title, #children, #required, #attributes.
function template_preprocess_datetime_wrapper(&$variables) {
  $element = $variables['element'];

  if (!empty($element['#title'])) {
    $variables['title'] = $element['#title'];
  if (!empty($element['#description'])) {
    $variables['description'] = $element['#description'];
  $title_attributes = array('class' => array('label'));
  // For required datetime fields a 'form-required' class is appended to the
  // label attributes.
  if (!empty($element['#required'])) {
    $title_attributes['class'][] = 'form-required';
  }
  $variables['title_attributes'] = new Attribute($title_attributes);
  $variables['content'] = $element['#children'];
 * Expands a datetime element type into date and/or time elements.
 *
 * All form elements are designed to have sane defaults so any or all can be
 * omitted. Both the date and time components are configurable so they can be
 * output as HTML5 datetime elements or not, as desired.
 *
 * Examples of possible configurations include:
 *   HTML5 date and time:
 *     #date_date_element = 'date';
 *     #date_time_element = 'time';
 *   HTML5 datetime:
 *     #date_date_element = 'datetime';
 *     #date_time_element = 'none';
 *   HTML5 time only:
 *     #date_date_element = 'none';
 *     #date_time_element = 'time'
 *   Non-HTML5:
 *     #date_date_element = 'text';
 *     #date_time_element = 'text';
 *
 * 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_date_format: A date format string that describes the format that
 *     should be displayed to the end user for the date. When using HTML5
 *     elements the format MUST use the appropriate HTML5 format for that
 *     element, no other format will work. See the format_date() function for a
 *     list of the possible formats and HTML5 standards for the HTML5
 *     requirements. Defaults to the right HTML5 format for the chosen element
 *     if a HTML5 element is used, otherwise defaults to
 *     entity_load('date_format', 'html_date')->getPattern().
 *   - #date_date_element: The date element. Options are:
 *     - datetime: Use the HTML5 datetime element type.
 *     - datetime-local: Use the HTML5 datetime-local element type.
 *     - date: Use the HTML5 date element type.
 *     - text: No HTML5 element, use a normal text field.
 *     - none: Do not display a date element.
 *   - #date_date_callbacks: Array of optional callbacks for the date element.
 *     Can be used to add a jQuery datepicker.
 *   - #date_time_element: The time element. Options are:
 *     - time: Use a HTML5 time element type.
 *     - text: No HTML5 element, use a normal text field.
 *     - none: Do not display a time element.
 *   - #date_time_format: A date format string that describes the format that
 *     should be displayed to the end user for the time. When using HTML5
 *     elements the format MUST use the appropriate HTML5 format for that
 *     element, no other format will work. See the format_date() function for
 *     a list of the possible formats and HTML5 standards for the HTML5
 *     requirements. Defaults to the right HTML5 format for the chosen element
 *     if a HTML5 element is used, otherwise defaults to
 *     entity_load('date_format', 'html_time')->getPattern().
 *   - #date_time_callbacks: An array of optional callbacks for the time
 *     element. Can be used to add a jQuery timepicker or an 'All day' checkbox.
 *   - #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. Used in jQueryUI datepicker year
 *     range and HTML5 min/max date settings. 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. Used for HTML5 step values and
 *     jQueryUI datepicker settings. Defaults to 1 to show every minute.
 *   - #date_timezone: The local timezone to use when creating dates. Generally
 *     this should be left empty and it will be set correctly for the user using
 *     the form. Useful if the default value is empty to designate a desired
 *     timezone for dates created in form processing. If a default date is
 *     provided, this value will be ignored, the timezone in the default date
 *     takes precedence. Defaults to the value returned by
 *     drupal_get_user_timezone().
 *
 * Example usage:
 * @code
 *   $form = array(
 *     '#type' => 'datetime',
 *     '#default_value' => new DrupalDateTime('2000-01-01 00:00:00'),
 *     '#date_date_element' => 'date',
 *     '#date_time_element' => 'none',
 *     '#date_year_range' => '2010:+3',
 *   );
 * @endcode
 *
 * @param array $element
 *   The form element whose value is being processed.
 * @param array $form_state
 *   The current state of the form.
 *
 * @return array
 *   The form element whose value has been processed.
 */
function datetime_datetime_form_process($element, &$form_state) {
  // The value callback has populated the #value array.
  $date = !empty($element['#value']['object']) ? $element['#value']['object'] : NULL;

  // Set a fallback timezone.
  if ($date instanceOf DrupalDateTime) {
    $element['#date_timezone'] = $date->getTimezone()->getName();
  }
  elseif (!empty($element['#timezone'])) {
    $element['#date_timezone'] = $element['#date_timezone'];
  }
  else {
    $element['#date_timezone'] = drupal_get_user_timezone();
  }

  $element['#tree'] = TRUE;

  if ($element['#date_date_element'] != 'none') {

    $date_format = $element['#date_date_element'] != 'none' ? datetime_html5_format('date', $element) : '';
    $date_value = !empty($date) ? $date->format($date_format, $format_settings) : $element['#value']['date'];

    // Creating format examples on every individual date item is messy, and
    // placeholders are invalid for HTML5 date and datetime, so an example
    // format is appended to the title to appear in tooltips.
    $extra_attributes = array(
      'title' => t('Date (i.e. !format)', array('!format' => datetime_format_example($date_format))),
      'type' => $element['#date_date_element'],
    );

    // Adds the HTML5 date attributes.
    if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
      $html5_min = clone($date);
      $range = datetime_range_years($element['#date_year_range'], $date);
      $html5_min->setDate($range[0], 1, 1)->setTime(0, 0, 0);
      $html5_max = clone($date);
      $html5_max->setDate($range[1], 12, 31)->setTime(23, 59, 59);

      $extra_attributes += array(
        'min' => $html5_min->format($date_format, $format_settings),
        'max' => $html5_max->format($date_format, $format_settings),
      );
    }

    $element['date'] = array(
      '#type' => 'date',
      '#title' => t('Date'),
      '#title_display' => 'invisible',
      '#value' => $date_value,
      '#attributes' => $element['#attributes'] + $extra_attributes,
      '#required' => $element['#required'],
      '#size' => max(12, strlen($element['#value']['date'])),
    );

    // Allows custom callbacks to alter the element.
    if (!empty($element['#date_date_callbacks'])) {
      foreach ($element['#date_date_callbacks'] as $callback) {
        if (function_exists($callback)) {
          $callback($element, $form_state, $date);
        }
      }
    }
  }

  if ($element['#date_time_element'] != 'none') {

    $time_format = $element['#date_time_element'] != 'none' ? datetime_html5_format('time', $element) : '';
    $time_value = !empty($date) ? $date->format($time_format, $format_settings) : $element['#value']['time'];

    // Adds the HTML5 attributes.
    $extra_attributes = array(
      'title' =>t('Time (i.e. !format)', array('!format' => datetime_format_example($time_format))),
      'type' => $element['#date_time_element'],
      'step' => $element['#date_increment'],
    );
    $element['time'] = array(
      '#type' => 'date',
      '#title' => t('Time'),
      '#title_display' => 'invisible',
      '#value' => $time_value,
      '#attributes' => $element['#attributes'] + $extra_attributes,
      '#required' => $element['#required'],
      '#size' => 12,
    );

    // Allows custom callbacks to alter the element.
    if (!empty($element['#date_time_callbacks'])) {
      foreach ($element['#date_time_callbacks'] as $callback) {
        if (function_exists($callback)) {
          $callback($element, $form_state, $date);
        }
      }
    }
  }

  return $element;
}

/**
 * Value callback for a datetime element.
 *
 * @param array $element
 *   The form element whose value is being populated.
 * @param array $input
 *   (optional) The incoming input to populate the form element. If this is
 *   FALSE, the element's default value should be returned. Defaults to FALSE.
 *
 * @return array
 *   The data that will appear in the $element_state['values'] collection for
 *   this element. Return nothing to use the default.
 */
function form_type_datetime_value($element, $input = FALSE) {
  if ($input !== FALSE) {
    $date_input  = $element['#date_date_element'] != 'none' && !empty($input['date']) ? $input['date'] : '';
    $time_input  = $element['#date_time_element'] != 'none' && !empty($input['time']) ? $input['time'] : '';
    $date_format = $element['#date_date_element'] != 'none' ? datetime_html5_format('date', $element) : '';
    $time_format = $element['#date_time_element'] != 'none' ? datetime_html5_format('time', $element) : '';
    $timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;

    // Seconds will be omitted in a post in case there's no entry.
    if (!empty($time_input) && strlen($time_input) == 5) {
      $time_input .= ':00';
    }

      $date_time_format = trim($date_format . ' ' . $time_format);
      $date_time_input = trim($date_input . ' ' . $time_input);
      $date = DrupalDateTime::createFromFormat($date_time_format, $date_time_input, $timezone);
    $input = array(
      'date'   => $date_input,
      'time'   => $time_input,
    );
  }
  else {
    $date = $element['#default_value'];
    if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
      $input = array(
        'date'   => $date->format($element['#date_date_format']),
        'time'   => $date->format($element['#date_time_format']),
        'object' => $date,
      );
    }
    else {
      $input = array(
        'date'   => '',
        'time'   => '',
        'object' => NULL,
      );
    }
  }
  return $input;
}

/**
 * Validation callback for a datetime 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 form element whose value is being validated.
 * @param array $form_state
 *   The current state of the form.
 */
function datetime_datetime_validate($element, &$form_state) {

  $input_exists = FALSE;
  $input = NestedArray::getValue($form_state['values'], $element['#parents'], $input_exists);
  if ($input_exists) {

    $title = !empty($element['#title']) ? $element['#title'] : '';
    $date_format = $element['#date_date_element'] != 'none' ? datetime_html5_format('date', $element) : '';
    $time_format = $element['#date_time_element'] != 'none' ? datetime_html5_format('time', $element) : '';
    $format = trim($date_format . ' ' . $time_format);

    // If there's empty input and the field is not required, set it to empty.
    if (empty($input['date']) && empty($input['time']) && !$element['#required']) {
      form_set_value($element, NULL, $form_state);
    }
    // If there's empty input and the field is required, set an error. A
    // reminder of the required format in the message provides a good UX.
    elseif (empty($input['date']) && empty($input['time']) && $element['#required']) {
      form_error($element, $form_state, t('The %field date is required. Please enter a date in the format %format.', array('%field' => $title, '%format' => datetime_format_example($format))));
    }
    else {
      // If the date is valid, set it.
      $date = $input['object'];
      if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
        form_set_value($element, $date, $form_state);
      }
      // If the date is invalid, set an error. A reminder of the required
      // format in the message provides a good UX.
      else {
        form_error($element, $form_state, t('The %field date is invalid. Please enter a date in the format %format.', array('%field' => $title, '%format' => datetime_format_example($format))));
      }
    }
  }
}

/**
 * Retrieves the right format for a HTML5 date element.
 *
 * The format is important because these elements will not work with any other
 * format.
 *
 * @param string $part
 *   The type of element format to retrieve.
 * @param string $element
 *   The $element to assess.
 *
 * @return string
 *   Returns the right format for the type of element, or the original format
 *   if this is not a HTML5 element.
 */
function datetime_html5_format($part, $element) {
  switch ($part) {
    case 'date':
      switch ($element['#date_date_element']) {
        case 'date':
          return entity_load('date_format', 'html_date')->getPattern();
          return entity_load('date_format', 'html_datetime')->getPattern();

        default:
          return $element['#date_date_format'];
      }
      break;

    case 'time':
      switch ($element['#date_time_element']) {
        case 'time':
          return entity_load('date_format', 'html_time')->getPattern();

        default:
          return $element['#date_time_format'];
      }
      break;
  }
}

/**
 * Creates an example for a date format.
 *
 * This is centralized for a consistent method of creating these examples.
 *
 * @param string $format
 *
 *
 * @return string
 *
 */
function datetime_format_example($format) {
  $date = &drupal_static(__FUNCTION__);
  if (empty($date)) {
    $date = new DrupalDateTime();
  }
  return $date->format($format);
}

/**
 * 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 array('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 local timezone to use when creating dates. Generally
 *     this should be left empty and it will be set correctly for the user using
 *     the form. Useful if the default value is empty to designate a desired
 *     timezone for dates created in form processing. If a default date is
 *     provided, this value will be ignored, the timezone in the default date
 *     takes precedence. Defaults to the value returned by
 *     drupal_get_user_timezone().
 *
 * Example usage:
 * @code
 *   $form = array(
 *     '#type' => 'datelist',
 *     '#default_value' => new DrupalDateTime('2000-01-01 00:00:00'),
 *     '#date_part_order' => array('month', 'day', 'year', 'hour', 'minute', 'ampm'),
 *     '#date_text_parts' => array('year'),
 *     '#date_year_range' => '2010:2020',
 *     '#date_increment' => 15,
 *   );
 * @endcode
 *
 * @param array $element
 *   The form element whose value is being processed.
 * @param array $form_state
 *   The current state of the form.
 */
function datetime_datelist_form_process($element, &$form_state) {

  // 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;

  // Set a fallback timezone.
  if ($date instanceOf DrupalDateTime) {
    $element['#date_timezone'] = $date->getTimezone()->getName();
  }
  elseif (!empty($element['#timezone'])) {
    $element['#date_timezone'] = $element['#date_timezone'];
  }
  else {
    $element['#date_timezone'] = drupal_get_user_timezone();
  }

  $element['#tree'] = TRUE;

  // Determine the order of the date elements.
  $order = !empty($element['#date_part_order']) ? $element['#date_part_order'] : array('year', 'month', 'day');
  $text_parts = !empty($element['#date_text_parts']) ? $element['#date_text_parts'] : array();

  // 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 = datetime_range_years($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');
    }

    $default = !empty($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] = array(
      '#type' => in_array($part, $text_parts) ? 'textfield' : 'select',
      '#title' => $title,
      '#title_display' => 'invisible',
      '#value' => $value,
      '#attributes' => $element['#attributes'],
      '#options' => $options,
      '#required' => $element['#required'],
    );
  }

  // Allows custom callbacks to alter the element.
  if (!empty($element['#date_date_callbacks'])) {
    foreach ($element['#date_date_callbacks'] as $callback) {
      if (function_exists($callback)) {
        $callback($element, $form_state, $date);
      }
    }
  }

  return $element;
}

/**
 * Element value callback for datelist element.
 *
 * 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.
 *
 * @param array $element
 *   The element being processed.
 * @param array|false $input
 *
 * @param array $form_state
 *   (optional) The current state of the form.  Defaults to an empty array.
 *
 * @return array
 *
 */
function form_type_datelist_value($element, $input = FALSE, &$form_state = array()) {
  $parts = $element['#date_part_order'];
  $increment = $element['#date_increment'];

  $date = NULL;
  if ($input !== FALSE) {
    $return = $input;
    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']);
    }
    $timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
    $date = DrupalDateTime::createFromArray($input, $timezone);
    if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
      date_increment_round($date, $increment);
    }
  }
  else {
    $return = array_fill_keys($parts, '');
    if (!empty($element['#default_value'])) {
      $date = $element['#default_value'];
      if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
        date_increment_round($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';
          }
          $return[$part] = $date->format($format);
        }
      }
    }
  }
  $return['object'] = $date;
  return $return;
}

/**
 * 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 array $form_state
 *   The current state of the form.
 */
function datetime_datelist_validate($element, &$form_state) {
  $input_exists = FALSE;
  $input = NestedArray::getValue($form_state['values'], $element['#parents'], $input_exists);
  if ($input_exists) {

    // 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_set_value($element, NULL, $form_state);
    }
    // If there's empty input and the field is required, set an error.
    elseif (empty($input['year']) && empty($input['month']) && empty($input['day']) && $element['#required']) {
      form_error($element, $form_state, t('The %field date is required.'));
    }
    else {
      // If the input is valid, set it.
      $date = $input['object'];
      if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
        form_set_value($element, $date, $form_state);
      }
      // If the input is invalid, set an error.
      else {
        form_error($element, $form_state, t('The %field date is invalid.'));
      }
    }
  }
}

/**
 * Rounds minutes and seconds to nearest requested value.
 *
 * @param $date
 *
 * @param $increment
 *
 *
 * @return
 *
 */
function date_increment_round(&$date, $increment) {
  // Round minutes and seconds, if necessary.
  if ($date instanceOf DrupalDateTime && $increment > 1) {
    $day = intval(date_format($date, 'j'));
    $hour = intval(date_format($date, 'H'));
    $second = intval(round(intval(date_format($date, 's')) / $increment) * $increment);
    $minute = intval(date_format($date, 'i'));
    if ($second == 60) {
      $minute += 1;
      $second = 0;
    }
    $minute = intval(round($minute / $increment) * $increment);
    if ($minute == 60) {
      $hour += 1;
      $minute = 0;
    }
    date_time_set($date, $hour, $minute, $second);
    if ($hour == 24) {
      $day += 1;
      $year = date_format($date, 'Y');
      $month = date_format($date, 'n');
      date_date_set($date, $year, $month, $day);
    }
  }
  return $date;
}

/**
 * Specifies the start and end year to use as a date range.
 *
 * Handles a string like -3:+3 or 2001:2010 to describe a dynamic range of
 * minimum and maximum years to use in a date selector.
 *
 * Centers the range around the current year, if any, but expands it far enough
 * so it will pick up the year value in the field in case the value in the field
 * is outside the initial range.
 *
 * @param string $string
 *   A min and max year string like '-3:+1' or '2000:2010' or '2000:+3'.
 * @param object $date
 *   (optional) A date object to test as a default value. Defaults to NULL.
 *
 * @return array
 *   A numerically indexed array, containing the minimum and maximum year
 *   described by this pattern.
 */
function datetime_range_years($string, $date = NULL) {

  $this_year = date_format(new DrupalDateTime(), 'Y');
  list($min_year, $max_year) = explode(':', $string);

  // Valid patterns would be -5:+5, 0:+1, 2008:2010.
  $plus_pattern = '@[\+|\-][0-9]{1,4}@';
  $year_pattern = '@^[0-9]{4}@';
  if (!preg_match($year_pattern, $min_year, $matches)) {
    if (preg_match($plus_pattern, $min_year, $matches)) {
      $min_year = $this_year + $matches[0];
    }
    else {
      $min_year = $this_year;
    }
  }
  if (!preg_match($year_pattern, $max_year, $matches)) {
    if (preg_match($plus_pattern, $max_year, $matches)) {
      $max_year = $this_year + $matches[0];
    }
    else {
      $max_year = $this_year;
    }
  }
  // We expect the $min year to be less than the $max year. Some custom values
  // for -99:+99 might not obey that.
  if ($min_year > $max_year) {
    $temp = $max_year;
    $max_year = $min_year;
    $min_year = $temp;
  }
  // If there is a current value, stretch the range to include it.
  $value_year = $date instanceOf DrupalDateTime ? $date->format('Y') : '';
  if (!empty($value_year)) {
    $min_year = min($value_year, $min_year);
    $max_year = max($value_year, $max_year);
  }
  return array($min_year, $max_year);
}