diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index b49c6c7d112637f5028261ded93872bda30128b5..248a07231066c9ab5b28849b7907142994e6cf39 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -382,6 +382,26 @@ core.base_field_override.*.*.*: type: field_config_base label: 'Base field bundle override' +core.date_format.*: + type: config_entity + label: 'Date format' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + locked: + type: boolean + label: 'Locked' + pattern: + type: date_format + label: 'PHP date format' + langcode: + type: string + label: 'Default language' + # Schema for the String field type. field.string.instance_settings: diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 0a6f475dedec2122892f301063d28fb8a8de2018..f26f9afbad0b49c7346a7fd429ca6da28e5be1fd 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -843,6 +843,68 @@ function template_preprocess_time(&$variables) { $variables['attributes'] = new Attribute($variables['attributes']); } +/** + * 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) { + $element = $variables['element']; + + $variables['attributes'] = array(); + if (isset($element['#id'])) { + $variables['attributes']['id'] = $element['#id']; + } + if (!empty($element['#attributes']['class'])) { + $variables['attributes']['class'] = (array) $element['#attributes']['class']; + } + $variables['attributes']['class'][] = 'container-inline'; + + $variables['content'] = $element; +} + +/** + * 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']; +} + /** * Prepares variables for status message templates. * @@ -2183,6 +2245,14 @@ function drupal_common_theme() { 'variables' => array('timestamp' => NULL, 'text' => NULL, 'attributes' => array(), 'html' => FALSE), 'template' => 'time', ), + 'datetime_form' => array( + 'template' => 'datetime-form', + 'render element' => 'element', + ), + 'datetime_wrapper' => array( + 'template' => 'datetime-wrapper', + 'render element' => 'element', + ), 'status_messages' => array( 'variables' => array('display' => NULL), 'template' => 'status-messages', diff --git a/core/modules/system/src/DateFormatInterface.php b/core/lib/Drupal/Core/Datetime/DateFormatInterface.php similarity index 90% rename from core/modules/system/src/DateFormatInterface.php rename to core/lib/Drupal/Core/Datetime/DateFormatInterface.php index fe1353d1a71c51b1007c661e426903c7bf1386cd..162ec3bf0469355d6aa409047327799ac30ccd97 100644 --- a/core/modules/system/src/DateFormatInterface.php +++ b/core/lib/Drupal/Core/Datetime/DateFormatInterface.php @@ -2,10 +2,10 @@ /** * @file - * Contains \Drupal\system\DateFormatInterface. + * Contains \Drupal\Core\Datetime\DateFormatInterface. */ -namespace Drupal\system; +namespace Drupal\Core\Datetime; use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Datetime\DrupalDateTime; diff --git a/core/modules/datetime/src/DateHelper.php b/core/lib/Drupal/Core/Datetime/DateHelper.php similarity index 99% rename from core/modules/datetime/src/DateHelper.php rename to core/lib/Drupal/Core/Datetime/DateHelper.php index f90d2c09445c8bc9921625431172c17f251b1657..1357267f5805e5cc41a0d207f28282880f68e9a4 100644 --- a/core/modules/datetime/src/DateHelper.php +++ b/core/lib/Drupal/Core/Datetime/DateHelper.php @@ -1,7 +1,7 @@ <?php /** * @file - * Contains \Drupal\datetime\DateHelper. + * Contains \Drupal\Core\Datetime\DateHelper. * * Lots of helpful functions for use in massaging dates, specific to the the * Gregorian calendar system. The values include both translated and @@ -14,7 +14,7 @@ * translation should be hard-coded and wrapped in t() so the translation system * will be able to process them. */ -namespace Drupal\datetime; +namespace Drupal\Core\Datetime; use Drupal\Core\Datetime\DrupalDateTime; diff --git a/core/lib/Drupal/Core/Datetime/Element/DateElementBase.php b/core/lib/Drupal/Core/Datetime/Element/DateElementBase.php new file mode 100644 index 0000000000000000000000000000000000000000..56eb9cf7b48ef9d8e3660bbdbb3d95186e672f9c --- /dev/null +++ b/core/lib/Drupal/Core/Datetime/Element/DateElementBase.php @@ -0,0 +1,77 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Datetime\Element\DateElementBase. + */ + +namespace Drupal\Core\Datetime\Element; + +use Drupal\Core\Datetime\DrupalDateTime; +use Drupal\Core\Render\Element\FormElement; + +/** + * Provides a base class for date elements. + */ +abstract class DateElementBase extends FormElement { + + /** + * 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. + */ + protected static function datetimeRangeYears($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); + } + +} + diff --git a/core/lib/Drupal/Core/Datetime/Element/Datelist.php b/core/lib/Drupal/Core/Datetime/Element/Datelist.php new file mode 100644 index 0000000000000000000000000000000000000000..c330a4f8cbc8060e3253f28ee5673da99a21c14d --- /dev/null +++ b/core/lib/Drupal/Core/Datetime/Element/Datelist.php @@ -0,0 +1,361 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Datetime\Element\Datelist. + */ + +namespace Drupal\Core\Datetime\Element; + +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Datetime\DateHelper; +use Drupal\Core\Datetime\DrupalDateTime; +use Drupal\Core\Form\FormStateInterface; + +/** + * Provides a datelist element. + * + * @FormElement("datelist") + */ +class Datelist extends DateElementBase { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#element_validate' => array( + array($class, 'validateDatelist'), + ), + '#process' => array( + array($class, 'processDatelist'), + ), + '#theme' => 'datetime_form', + '#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' => '', + ); + } + + /** + * {@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 (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()) { + 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()) { + 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 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 \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; + + // 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 = 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 = array(); + $title = ''; + } + + $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; + } + + /** + * 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); + 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_state->setValueForElement($element, NULL); + } + // 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_state->setError($element, t('The %field date is required.')); + } + else { + // If the input is valid, set it. + $date = $input['object']; + if ($date instanceOf DrupalDateTime && !$date->hasErrors()) { + $form_state->setValueForElement($element, $date); + } + // If the input is invalid, set an error. + else { + $form_state->setError($element, t('The %field date is invalid.')); + } + } + } + } + + /** + * Rounds minutes and seconds to nearest requested value. + * + * @param $date + * + * @param $increment + * + * @return + */ + protected static function incrementRound(&$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; + } + +} diff --git a/core/lib/Drupal/Core/Datetime/Element/Datetime.php b/core/lib/Drupal/Core/Datetime/Element/Datetime.php new file mode 100644 index 0000000000000000000000000000000000000000..583372b3dd868866c6a9531087319e802f8bef62 --- /dev/null +++ b/core/lib/Drupal/Core/Datetime/Element/Datetime.php @@ -0,0 +1,431 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Datetime\Element\Datetime. + */ + +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 array( + '#input' => TRUE, + '#element_validate' => array( + array($class, 'validateDatetime'), + ), + '#process' => array( + array($class, 'processDatetime'), + array($class, 'processGroup'), + ), + '#pre_render' => array( + array($class, 'preRenderGroup'), + ), + '#theme' => 'datetime_form', + '#theme_wrappers' => array('datetime_wrapper'), + '#date_date_format' => $date_format, + '#date_date_element' => 'date', + '#date_date_callbacks' => array(), + '#date_time_format' => $time_format, + '#date_time_element' => 'time', + '#date_time_callbacks' => array(), + '#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 = array( + 'date' => $date_input, + 'time' => $time_input, + 'object' => $date, + ); + } + 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; + } + + /** + * 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 \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 = array(); + // 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 = array( + 'title' => t('Date (i.e. !format)', array('!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 += 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' ? static::getHtml5TimeFormat($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' => static::formatExample($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; + } + + /** + * 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['values'], $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.', array('%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.', array('%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']; + } + } + +} diff --git a/core/modules/system/src/Entity/DateFormat.php b/core/lib/Drupal/Core/Datetime/Entity/DateFormat.php similarity index 74% rename from core/modules/system/src/Entity/DateFormat.php rename to core/lib/Drupal/Core/Datetime/Entity/DateFormat.php index 67614d2513225ce7cc7691ee8dd2a6d1005773fb..ed40b900acbf8aea0946f6327bde6d85acfd6c7e 100644 --- a/core/modules/system/src/Entity/DateFormat.php +++ b/core/lib/Drupal/Core/Datetime/Entity/DateFormat.php @@ -2,15 +2,14 @@ /** * @file - * Contains \Drupal\system\Entity\DateFormat. + * Contains \Drupal\Core\Datetime\Entity\DateFormat. */ -namespace Drupal\system\Entity; +namespace Drupal\Core\Datetime\Entity; use Drupal\Core\Config\Entity\ConfigEntityBase; -use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Config\Entity\ConfigEntityInterface; -use Drupal\system\DateFormatInterface; +use Drupal\Core\Datetime\DateFormatInterface; /** * Defines the Date Format configuration entity class. @@ -20,22 +19,12 @@ * label = @Translation("Date format"), * handlers = { * "access" = "Drupal\system\DateFormatAccessControlHandler", - * "list_builder" = "Drupal\system\DateFormatListBuilder", - * "form" = { - * "add" = "Drupal\system\Form\DateFormatAddForm", - * "edit" = "Drupal\system\Form\DateFormatEditForm", - * "delete" = "Drupal\system\Form\DateFormatDeleteForm" - * } * }, * entity_keys = { * "id" = "id", * "label" = "label" * }, * admin_permission = "administer site configuration", - * links = { - * "delete-form" = "entity.date_format.delete_form", - * "edit-form" = "entity.date_format.edit_form" - * } * ) */ class DateFormat extends ConfigEntityBase implements DateFormatInterface { diff --git a/core/lib/Drupal/Core/Datetime/Plugin/README.txt b/core/lib/Drupal/Core/Datetime/Plugin/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..f761e0198cef57dbae101045b12e2bc350d208a9 --- /dev/null +++ b/core/lib/Drupal/Core/Datetime/Plugin/README.txt @@ -0,0 +1,4 @@ +@todo This must be here because DrupalKernel will only allow namespaces to + provide plugins if there is a Plugin subdirectory, and git does not allow + empty subdirectories. This file should be removed once + https://www.drupal.org/node/2309889 is fixed. diff --git a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php index ab2a1f8f75068e17c7563bcbd81a4215fbd903dc..253af4876c5fc5c4011512c5a733b1783500dc30 100644 --- a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php +++ b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php @@ -397,15 +397,15 @@ public function testDateFormatTranslation() { // Update translatable fields. $edit = array( - 'config_names[system.date_format.' . $id . '][label][translation]' => $id . ' - FR', - 'config_names[system.date_format.' . $id . '][pattern][translation]' => 'D', + 'config_names[core.date_format.' . $id . '][label][translation]' => $id . ' - FR', + 'config_names[core.date_format.' . $id . '][pattern][translation]' => 'D', ); // Save language specific version of form. $this->drupalPostForm($translation_page_url, $edit, t('Save translation')); // Get translation and check we've got the right value. - $override = \Drupal::languageManager()->getLanguageConfigOverride('fr', 'system.date_format.' . $id); + $override = \Drupal::languageManager()->getLanguageConfigOverride('fr', 'core.date_format.' . $id); $expected = array( 'label' => $id . ' - FR', 'pattern' => 'D', diff --git a/core/modules/datetime/datetime.module b/core/modules/datetime/datetime.module index 99cf2fd576a1469219c109f45bca9296779da73e..cca729f69b722c874168fce916975cb097282d13 100644 --- a/core/modules/datetime/datetime.module +++ b/core/modules/datetime/datetime.module @@ -10,7 +10,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Template\Attribute; -use Drupal\datetime\DateHelper; +use Drupal\Core\Datetime\Element\Datetime; use Drupal\node\NodeInterface; /** @@ -48,74 +48,6 @@ function datetime_help($route_name, RouteMatchInterface $route_match) { } } -/** - * 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', - array('Drupal\Core\Render\Element\RenderElement', 'processGroup'), - ), - '#pre_render' => array( - array('Drupal\Core\Render\Element\RenderElement', 'preRenderGroup'), - ), - '#theme' => 'datetime_form', - '#theme_wrappers' => array('datetime_wrapper'), - '#date_date_format' => $date_format, - '#date_date_element' => 'date', - '#date_date_callbacks' => array(), - '#date_time_format' => $time_format, - '#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' => 'datetime_form', - '#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( - 'template' => 'datetime-form', - 'render element' => 'element', - ), - 'datetime_wrapper' => array( - 'template' => 'datetime-wrapper', - 'render element' => 'element', - ), - ); -} - /** * Validation callback for the datetime widget element. * @@ -206,802 +138,6 @@ 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) { - $element = $variables['element']; - - $variables['attributes'] = array(); - if (isset($element['#id'])) { - $variables['attributes']['id'] = $element['#id']; - } - if (!empty($element['#attributes']['class'])) { - $variables['attributes']['class'] = (array) $element['#attributes']['class']; - } - $variables['attributes']['class'][] = 'container-inline'; - - $variables['content'] = $element; -} - -/** - * 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 \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return array - * The form element whose value has been processed. - */ -function datetime_datetime_form_process($element, FormStateInterface $form_state) { - $format_settings = array(); - // 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'; - } - - 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 = array( - 'date' => $date_input, - 'time' => $time_input, - 'object' => $date, - ); - } - 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 \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ -function datetime_datetime_validate($element, FormStateInterface $form_state) { - - $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' ? 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(); - - case 'datetime': - case 'datetime-local': - 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 \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ -function datetime_datelist_form_process($element, FormStateInterface $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 \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return array - */ -function form_type_datelist_value($element, $input = FALSE, FormStateInterface $form_state) { - $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 \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ -function datetime_datelist_validate($element, FormStateInterface $form_state) { - $input_exists = FALSE; - $input = NestedArray::getValue($form_state->getValues(), $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); -} - /** * Implements hook_form_BASE_FORM_ID_alter() for node forms. */ @@ -1010,7 +146,7 @@ function datetime_form_node_form_alter(&$form, FormStateInterface $form_state, $ $form['created']['#type'] = 'datetime'; $date_format = entity_load('date_format', 'html_date')->getPattern(); $time_format = entity_load('date_format', 'html_time')->getPattern(); - $form['created']['#description'] = t('Format: %format. Leave blank to use the time of form submission.', array('%format' => datetime_format_example($date_format . ' ' . $time_format))); + $form['created']['#description'] = t('Format: %format. Leave blank to use the time of form submission.', array('%format' => Datetime::formatExample($date_format . ' ' . $time_format))); unset($form['created']['#maxlength']); } diff --git a/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php b/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php index bd30b441009e2ddc217af561e281ccb3972f3de8..ac6bb2d7f8ceae2a36bee8fb522ed920c06d1dc3 100644 --- a/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php +++ b/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php @@ -110,7 +110,7 @@ public function testConfigTranslation() { ); $this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations')); - $wrapper = $this->container->get('locale.config.typed')->get('system.date_format.medium'); + $wrapper = $this->container->get('locale.config.typed')->get('core.date_format.medium'); // Get translation and check we've only got the site name. $translation = $wrapper->getTranslation($langcode); diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityDateFormat.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityDateFormat.php index 695056aa924348fae479a1fb847a849acc541a96..d471ceac0b33a4276ba7fd1967592f1b51af523f 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityDateFormat.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityDateFormat.php @@ -19,7 +19,7 @@ class EntityDateFormat extends EntityConfigBase { /** * {@inheritdoc} * - * @param \Drupal\system\DateFormatInterface $entity + * @param \Drupal\Core\Datetime\DateFormatInterface $entity * The date entity. */ protected function updateEntityProperty(EntityInterface $entity, array $parents, $value) { diff --git a/core/modules/migrate_drupal/src/Tests/d6/MigrateDateFormatTest.php b/core/modules/migrate_drupal/src/Tests/d6/MigrateDateFormatTest.php index b1ec34be709ca84d066308526586ae09ff9e2d16..247f00f8909b5a79e0cd5ecd8e16c3e673522a0d 100644 --- a/core/modules/migrate_drupal/src/Tests/d6/MigrateDateFormatTest.php +++ b/core/modules/migrate_drupal/src/Tests/d6/MigrateDateFormatTest.php @@ -9,7 +9,7 @@ use Drupal\Core\Database\Database; /** - * Upgrade date formats to system.date_format.*.yml. + * Upgrade date formats to core.date_format.*.yml. * * @group migrate_drupal */ diff --git a/core/modules/system/config/install/system.date_format.fallback.yml b/core/modules/system/config/install/core.date_format.fallback.yml similarity index 100% rename from core/modules/system/config/install/system.date_format.fallback.yml rename to core/modules/system/config/install/core.date_format.fallback.yml diff --git a/core/modules/system/config/install/system.date_format.html_date.yml b/core/modules/system/config/install/core.date_format.html_date.yml similarity index 100% rename from core/modules/system/config/install/system.date_format.html_date.yml rename to core/modules/system/config/install/core.date_format.html_date.yml diff --git a/core/modules/system/config/install/system.date_format.html_datetime.yml b/core/modules/system/config/install/core.date_format.html_datetime.yml similarity index 100% rename from core/modules/system/config/install/system.date_format.html_datetime.yml rename to core/modules/system/config/install/core.date_format.html_datetime.yml diff --git a/core/modules/system/config/install/system.date_format.html_month.yml b/core/modules/system/config/install/core.date_format.html_month.yml similarity index 100% rename from core/modules/system/config/install/system.date_format.html_month.yml rename to core/modules/system/config/install/core.date_format.html_month.yml diff --git a/core/modules/system/config/install/system.date_format.html_time.yml b/core/modules/system/config/install/core.date_format.html_time.yml similarity index 100% rename from core/modules/system/config/install/system.date_format.html_time.yml rename to core/modules/system/config/install/core.date_format.html_time.yml diff --git a/core/modules/system/config/install/system.date_format.html_week.yml b/core/modules/system/config/install/core.date_format.html_week.yml similarity index 100% rename from core/modules/system/config/install/system.date_format.html_week.yml rename to core/modules/system/config/install/core.date_format.html_week.yml diff --git a/core/modules/system/config/install/system.date_format.html_year.yml b/core/modules/system/config/install/core.date_format.html_year.yml similarity index 100% rename from core/modules/system/config/install/system.date_format.html_year.yml rename to core/modules/system/config/install/core.date_format.html_year.yml diff --git a/core/modules/system/config/install/system.date_format.html_yearless_date.yml b/core/modules/system/config/install/core.date_format.html_yearless_date.yml similarity index 100% rename from core/modules/system/config/install/system.date_format.html_yearless_date.yml rename to core/modules/system/config/install/core.date_format.html_yearless_date.yml diff --git a/core/modules/system/config/install/system.date_format.long.yml b/core/modules/system/config/install/core.date_format.long.yml similarity index 100% rename from core/modules/system/config/install/system.date_format.long.yml rename to core/modules/system/config/install/core.date_format.long.yml diff --git a/core/modules/system/config/install/system.date_format.medium.yml b/core/modules/system/config/install/core.date_format.medium.yml similarity index 100% rename from core/modules/system/config/install/system.date_format.medium.yml rename to core/modules/system/config/install/core.date_format.medium.yml diff --git a/core/modules/system/config/install/system.date_format.short.yml b/core/modules/system/config/install/core.date_format.short.yml similarity index 100% rename from core/modules/system/config/install/system.date_format.short.yml rename to core/modules/system/config/install/core.date_format.short.yml diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml index 2e9219d42b1bb1a42cbf237bd563bc3b713d578e..3f5ecbcace14c06fec9a55b8318c4bcdf36bd89b 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -114,26 +114,6 @@ system.date: type: boolean label: 'Remind users at login if their time zone is not set' -system.date_format.*: - type: config_entity - label: 'Date format' - mapping: - id: - type: string - label: 'ID' - label: - type: label - label: 'Label' - locked: - type: boolean - label: 'Locked' - pattern: - type: date_format - label: 'PHP date format' - langcode: - type: string - label: 'Default language' - system.diff: type: mapping label: 'Diff settings' diff --git a/core/modules/system/src/Tests/Extension/ThemeHandlerTest.php b/core/modules/system/src/Tests/Extension/ThemeHandlerTest.php index 75d10fd9f1619159b86a4c5c8e855c5643eb1303..f652bfdc495b87ad6663b185a7fc8e69f03ccdf1 100644 --- a/core/modules/system/src/Tests/Extension/ThemeHandlerTest.php +++ b/core/modules/system/src/Tests/Extension/ThemeHandlerTest.php @@ -186,14 +186,14 @@ function testDisableEnable() { $this->themeHandler()->disable(array($name)); $this->assertIdentical($this->config("$name.settings")->get('base'), 'only'); - $this->assertIdentical($this->config('system.date_format.fancy')->get('label'), 'Fancy date'); + $this->assertIdentical($this->config('core.date_format.fancy')->get('label'), 'Fancy date'); // Default configuration never overwrites custom configuration, so just // changing values in existing configuration will cause ConfigInstaller to // simply skip those files. To ensure that no default configuration is // re-imported, the custom configuration has to be deleted. $this->configStorage()->delete("$name.settings"); - $this->configStorage()->delete('system.date_format.fancy'); + $this->configStorage()->delete('core.date_format.fancy'); // Reflect direct storage operations in ConfigFactory. $this->container->get('config.factory')->reset(); @@ -206,7 +206,7 @@ function testDisableEnable() { $this->assertEqual(array_keys(system_list('theme')), array_keys($themes)); $this->assertFalse($this->config("$name.settings")->get()); - $this->assertNull($this->config('system.date_format.fancy')->get('label')); + $this->assertNull($this->config('core.date_format.fancy')->get('label')); } /** diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 4c6bc602e23742e2f8ea049ac2c21a9be5d2c821..f7af74f89211cd30b0f0f8436859a33eab6145f8 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1277,6 +1277,20 @@ function system_retrieve_file($url, $destination = NULL, $managed = FALSE, $repl return $local; } +/** + * Implements hook_entity_type_build(). + */ +function system_entity_type_build(array &$entity_types) { + /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */ + $entity_types['date_format'] + ->setFormClass('add', 'Drupal\system\Form\DateFormatAddForm') + ->setFormClass('edit', 'Drupal\system\Form\DateFormatEditForm') + ->setFormClass('delete', 'Drupal\system\Form\DateFormatDeleteForm') + ->setListBuilderClass('Drupal\system\DateFormatListBuilder') + ->setLinkTemplate('edit-form', 'entity.date_format.edit_form') + ->setLinkTemplate('delete-form', 'entity.date_format.delete_form'); +} + /** * Implements hook_page_alter(). */ diff --git a/core/modules/datetime/templates/datetime-form.html.twig b/core/modules/system/templates/datetime-form.html.twig similarity index 100% rename from core/modules/datetime/templates/datetime-form.html.twig rename to core/modules/system/templates/datetime-form.html.twig diff --git a/core/modules/datetime/templates/datetime-wrapper.html.twig b/core/modules/system/templates/datetime-wrapper.html.twig similarity index 100% rename from core/modules/datetime/templates/datetime-wrapper.html.twig rename to core/modules/system/templates/datetime-wrapper.html.twig diff --git a/core/modules/system/tests/themes/test_basetheme/config/install/system.date_format.fancy.yml b/core/modules/system/tests/themes/test_basetheme/config/install/core.date_format.fancy.yml similarity index 100% rename from core/modules/system/tests/themes/test_basetheme/config/install/system.date_format.fancy.yml rename to core/modules/system/tests/themes/test_basetheme/config/install/core.date_format.fancy.yml