Commit 12ee21a3 authored by webchick's avatar webchick
Browse files

Issue #2328061 by tim.plunkett: Move datetime's FormElement #type classes in Core.

parent 3467f054
......@@ -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:
......
......@@ -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',
......
......@@ -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;
......
<?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;
......
<?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);
}
}
<?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;
}
}
<?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 {