123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- <?php
- namespace Drupal\Core\Datetime\Element;
- use Drupal\Component\Utility\NestedArray;
- use Drupal\Core\Datetime\DrupalDateTime;
- use Drupal\Core\Form\FormStateInterface;
- use Drupal\Core\Datetime\Entity\DateFormat;
- /**
- * Provides a datetime element.
- *
- * @FormElement("datetime")
- */
- class Datetime extends DateElementBase {
- /**
- * @var \DateTimeInterface
- */
- protected static $dateExample;
- /**
- * {@inheritdoc}
- */
- public function getInfo() {
- $date_format = '';
- $time_format = '';
- // Date formats cannot be loaded during install or update.
- if (!defined('MAINTENANCE_MODE')) {
- if ($date_format_entity = DateFormat::load('html_date')) {
- /** @var $date_format_entity \Drupal\Core\Datetime\DateFormatInterface */
- $date_format = $date_format_entity->getPattern();
- }
- if ($time_format_entity = DateFormat::load('html_time')) {
- /** @var $time_format_entity \Drupal\Core\Datetime\DateFormatInterface */
- $time_format = $time_format_entity->getPattern();
- }
- }
- $class = get_class($this);
- return [
- '#input' => TRUE,
- '#element_validate' => [
- [$class, 'validateDatetime'],
- ],
- '#process' => [
- [$class, 'processDatetime'],
- [$class, 'processAjaxForm'],
- [$class, 'processGroup'],
- ],
- '#pre_render' => [
- [$class, 'preRenderGroup'],
- ],
- '#theme' => 'datetime_form',
- '#theme_wrappers' => ['datetime_wrapper'],
- '#date_date_format' => $date_format,
- '#date_date_element' => 'date',
- '#date_date_callbacks' => [],
- '#date_time_format' => $time_format,
- '#date_time_element' => 'time',
- '#date_time_callbacks' => [],
- '#date_year_range' => '1900:2050',
- '#date_increment' => 1,
- '#date_timezone' => '',
- ];
- }
- /**
- * {@inheritdoc}
- */
- public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
- 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' ? static::getHtml5DateFormat($element) : '';
- $time_format = $element['#date_time_element'] != 'none' ? static::getHtml5TimeFormat($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';
- }
- try {
- $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);
- }
- catch (\Exception $e) {
- $date = NULL;
- }
- $input = [
- 'date' => $date_input,
- 'time' => $time_input,
- 'object' => $date,
- ];
- }
- else {
- $date = $element['#default_value'];
- if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
- $input = [
- 'date' => $date->format($element['#date_date_format']),
- 'time' => $date->format($element['#date_time_format']),
- 'object' => $date,
- ];
- }
- else {
- $input = [
- 'date' => '',
- 'time' => '',
- 'object' => NULL,
- ];
- }
- }
- return $input;
- }
- /**
- * 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
- * DateFormat::load('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
- * DateFormat::load('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 \Drupal\Core\Form\FormStateInterface $form_state
- * The current state of the form.
- * @param array $complete_form
- * The complete form structure.
- *
- * @return array
- * The form element whose value has been processed.
- */
- public static function processDatetime(&$element, FormStateInterface $form_state, &$complete_form) {
- $format_settings = [];
- // 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'] = drupal_get_user_timezone();
- }
- $element['#tree'] = TRUE;
- if ($element['#date_date_element'] != 'none') {
- $date_format = $element['#date_date_element'] != 'none' ? static::getHtml5DateFormat($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 = [
- 'title' => t('Date (e.g. @format)', ['@format' => static::formatExample($date_format)]),
- 'type' => $element['#date_date_element'],
- ];
- // Adds the HTML5 date attributes.
- if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
- $html5_min = clone($date);
- $range = static::datetimeRangeYears($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 += [
- 'min' => $html5_min->format($date_format, $format_settings),
- 'max' => $html5_max->format($date_format, $format_settings),
- ];
- }
- $element['date'] = [
- '#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'])),
- '#error_no_message' => TRUE,
- '#date_date_format' => $element['#date_date_format'],
- ];
- // 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' ? static::getHtml5TimeFormat($element) : '';
- $time_value = !empty($date) ? $date->format($time_format, $format_settings) : $element['#value']['time'];
- // Adds the HTML5 attributes.
- $extra_attributes = [
- 'title' => t('Time (e.g. @format)', ['@format' => static::formatExample($time_format)]),
- 'type' => $element['#date_time_element'],
- 'step' => $element['#date_increment'],
- ];
- $element['time'] = [
- '#type' => 'date',
- '#title' => t('Time'),
- '#title_display' => 'invisible',
- '#value' => $time_value,
- '#attributes' => $element['#attributes'] + $extra_attributes,
- '#required' => $element['#required'],
- '#size' => 12,
- '#error_no_message' => TRUE,
- ];
- // 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;
- }
- /**
- * {@inheritdoc}
- */
- public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) {
- $element = parent::processAjaxForm($element, $form_state, $complete_form);
- // Copy the #ajax settings to the child elements.
- if (isset($element['#ajax'])) {
- if (isset($element['date'])) {
- $element['date']['#ajax'] = $element['#ajax'];
- }
- if (isset($element['time'])) {
- $element['time']['#ajax'] = $element['#ajax'];
- }
- }
- return $element;
- }
- /**
- * 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 \Drupal\Core\Form\FormStateInterface $form_state
- * The current state of the form.
- * @param array $complete_form
- * The complete form structure.
- */
- public static function validateDatetime(&$element, FormStateInterface $form_state, &$complete_form) {
- $input_exists = FALSE;
- $input = NestedArray::getValue($form_state->getValues(), $element['#parents'], $input_exists);
- if ($input_exists) {
- $title = !empty($element['#title']) ? $element['#title'] : '';
- $date_format = $element['#date_date_element'] != 'none' ? static::getHtml5DateFormat($element) : '';
- $time_format = $element['#date_time_element'] != 'none' ? static::getHtml5TimeFormat($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_state->setValueForElement($element, NULL);
- }
- // 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_state->setError($element, t('The %field date is required. Please enter a date in the format %format.', ['%field' => $title, '%format' => static::formatExample($format)]));
- }
- else {
- // If the date is valid, set it.
- $date = $input['object'];
- if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
- $form_state->setValueForElement($element, $date);
- }
- // If the date is invalid, set an error. A reminder of the required
- // format in the message provides a good UX.
- else {
- $form_state->setError($element, t('The %field date is invalid. Please enter a date in the format %format.', ['%field' => $title, '%format' => static::formatExample($format)]));
- }
- }
- }
- }
- /**
- * Creates an example for a date format.
- *
- * This is centralized for a consistent method of creating these examples.
- *
- * @param string $format
- *
- * @return string
- */
- public static function formatExample($format) {
- if (!static::$dateExample) {
- static::$dateExample = new DrupalDateTime();
- }
- return static::$dateExample->format($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 $element
- * The $element to assess.
- *
- * @return string
- * Returns the right format for the date element, or the original format
- * if this is not a HTML5 element.
- */
- protected static function getHtml5DateFormat($element) {
- switch ($element['#date_date_element']) {
- case 'date':
- return DateFormat::load('html_date')->getPattern();
- case 'datetime':
- case 'datetime-local':
- return DateFormat::load('html_datetime')->getPattern();
- default:
- return $element['#date_date_format'];
- }
- }
- /**
- * Retrieves the right format for a HTML5 time element.
- *
- * The format is important because these elements will not work with any other
- * format.
- *
- * @param string $element
- * The $element to assess.
- *
- * @return string
- * Returns the right format for the time element, or the original format
- * if this is not a HTML5 element.
- */
- protected static function getHtml5TimeFormat($element) {
- switch ($element['#date_time_element']) {
- case 'time':
- return DateFormat::load('html_time')->getPattern();
- default:
- return $element['#date_time_format'];
- }
- }
- }
|