diff --git a/core/modules/datetime/config/schema/datetime.schema.yml b/core/modules/datetime/config/schema/datetime.schema.yml
index 406a2fd..aade88f 100644
--- a/core/modules/datetime/config/schema/datetime.schema.yml
+++ b/core/modules/datetime/config/schema/datetime.schema.yml
@@ -1,5 +1,7 @@
# Schema for the configuration files of the Datetime module.
+# datetime
+
field.storage_settings.datetime:
type: mapping
label: 'Datetime settings'
@@ -83,3 +85,78 @@ field.widget.settings.datetime_datelist:
field.widget.settings.datetime_default:
type: mapping
label: 'Datetime default display format settings'
+
+# daterange
+
+field.storage_settings.daterange:
+ type: mapping
+ label: 'Daterange settings'
+ mapping:
+ datetime_type:
+ type: string
+ label: 'Date type'
+
+field.field_settings.daterange:
+ type: mapping
+ label: 'Daterange settings'
+
+field.value.daterange:
+ type: mapping
+ label: 'Default value'
+ mapping:
+ default_date_type:
+ type: string
+ label: 'Default date type'
+ default_date:
+ type: string
+ label: 'Default date value'
+
+field.formatter.settings.daterange_base:
+ type: mapping
+ mapping:
+ separator:
+ type: string
+ label: 'Separator'
+ timezone_override:
+ type: string
+ label: 'Time zone override'
+
+field.formatter.settings.daterange_default:
+ type: field.formatter.settings.daterange_base
+ label: 'Daterange default display format settings'
+ mapping:
+ format_type:
+ type: string
+ label: 'Date format'
+
+field.formatter.settings.daterange_plain:
+ type: field.formatter.settings.daterange_base
+ label: 'Daterange plain display format settings'
+
+field.formatter.settings.daterange_custom:
+ type: field.formatter.settings.daterange_base
+ label: 'Daterange custom display format settings'
+ mapping:
+ date_format:
+ type: string
+ label: 'Date/time format'
+ translatable: true
+ translation context: 'PHP date format'
+
+field.widget.settings.daterange_datelist:
+ type: mapping
+ label: 'Daterange select list display format settings'
+ mapping:
+ increment:
+ type: integer
+ label: 'Time increments'
+ date_order:
+ type: string
+ label: 'Date part order'
+ time_type:
+ type: string
+ label: 'Time type'
+
+field.widget.settings.daterange_default:
+ type: mapping
+ label: 'Daterange default display format settings'
diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php
new file mode 100644
index 0000000..3586fb7
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php
@@ -0,0 +1,106 @@
+ DATETIME_DATETIME_STORAGE_FORMAT,
+ ) + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function viewElements(FieldItemListInterface $items, $langcode) {
+ $elements = [];
+ $separator = $this->getSetting('separator');
+
+ foreach ($items as $delta => $item) {
+ if ($item->start_date && $item->end_date) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+ $start_date = $item->start_date;
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+ $end_date = $item->end_date;
+
+ if ($this->getFieldSetting('daterange_type') == 'date') {
+ // A date without time will pick up the current time, use the default.
+ datetime_date_default_time($start_date);
+ datetime_date_default_time($end_date);
+ }
+
+ $this->setTimeZone($start_date);
+ $this->setTimeZone($end_date);
+
+ $elements[$delta] = [
+ '#cache' => [
+ 'contexts' => [
+ 'timezone',
+ ],
+ ],
+ '#plain_text' => $this->formatDate($start_date) . ' ' . $separator . ' ' . $this->formatDate($end_date),
+ ];
+ }
+ }
+
+ return $elements;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function formatDate($date) {
+ $format = $this->getSetting('date_format');
+ $timezone = $this->getSetting('timezone_override');
+ return $this->dateFormatter->format($date->getTimestamp(), 'custom', $format, $timezone != '' ? $timezone : NULL);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ $form = parent::settingsForm($form, $form_state);
+
+ $form['date_format'] = array(
+ '#type' => 'textfield',
+ '#title' => $this->t('Date/time format'),
+ '#description' => $this->t('See the documentation for PHP date formats.'),
+ '#default_value' => $this->getSetting('date_format'),
+ );
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsSummary() {
+ $summary = parent::settingsSummary();
+
+ $date = DrupalDateTime::createFromTimestamp(REQUEST_TIME);
+ $this->setTimeZone($date);
+ $summary[] = $this->t('Format: @display', array('@display' => $this->formatDate($date)));
+
+ return $summary;
+ }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php
new file mode 100644
index 0000000..e239936
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php
@@ -0,0 +1,141 @@
+ 'medium',
+ ) + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function viewElements(FieldItemListInterface $items, $langcode) {
+ $elements = [];
+ $separator = $this->getSetting('separator');
+
+ foreach ($items as $delta => $item) {
+ if ($item->start_date && $item->end_date) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+ $start_date = $item->start_date;
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+ $end_date = $item->end_date;
+
+ if ($this->getFieldSetting('daterange_type') == 'date') {
+ // A date without time will pick up the current time, use the default.
+ datetime_date_default_time($start_date);
+ datetime_date_default_time($end_date);
+ }
+
+ // Create the ISO dates in Universal Time.
+ $start_iso_date = $start_date->format("Y-m-d\TH:i:s") . 'Z';
+ $end_iso_date = $end_date->format("Y-m-d\TH:i:s") . 'Z';
+
+ $this->setTimeZone($start_date);
+ $this->setTimeZone($end_date);
+
+ // Display the dates using theme datetime.
+ $elements[$delta] = [
+ '#cache' => [
+ 'contexts' => [
+ 'timezone',
+ ],
+ ],[
+ '#theme' => 'time',
+ '#text' => $this->formatDate($start_date),
+ '#html' => FALSE,
+ '#attributes' => [
+ 'datetime' => $start_iso_date,
+ ]
+ ],[
+ '#plain_text' => ' ' . $separator . ' ',
+ ],[
+ '#theme' => 'time',
+ '#text' => $this->formatDate($end_date),
+ '#html' => FALSE,
+ '#attributes' => [
+ 'datetime' => $end_iso_date,
+ ]
+ ],
+ ];
+ if (!empty($item->_attributes)) {
+ $elements[$delta]['#attributes'] += $item->_attributes;
+ // Unset field item attributes since they have been included in the
+ // formatter output and should not be rendered in the field template.
+ unset($item->_attributes);
+ }
+ }
+ }
+
+ return $elements;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function formatDate($date) {
+ $format_type = $this->getSetting('format_type');
+ $timezone = $this->getSetting('timezone_override');
+ return $this->dateFormatter->format($date->getTimestamp(), $format_type, '', $timezone != '' ? $timezone : NULL);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ $form = parent::settingsForm($form, $form_state);
+
+ $format_types = $this->dateFormatStorage->loadMultiple();
+ $options = [];
+
+ foreach ($format_types as $type => $type_info) {
+ $format = $this->dateFormatter->format(REQUEST_TIME, $type);
+ $options[$type] = $type_info->label() . ' (' . $format . ')';
+ }
+
+ $form['format_type'] = array(
+ '#type' => 'select',
+ '#title' => t('Date format'),
+ '#description' => $this->t('Choose a format for displaying the dates. Be sure to set a format appropriate for the field, i.e. omitting time for a field that only has a date.'),
+ '#options' => $options,
+ '#default_value' => $this->getSetting('format_type'),
+ );
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsSummary() {
+ $summary = parent::settingsSummary();
+
+ $date = DrupalDateTime::createFromTimestamp(REQUEST_TIME);
+ $this->setTimeZone($date);
+ $summary[] = $this->t('Format: @display', array('@display' => $this->formatDate($date)));
+
+ return $summary;
+ }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeFormatterBase.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeFormatterBase.php
new file mode 100644
index 0000000..f0b6b04
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeFormatterBase.php
@@ -0,0 +1,174 @@
+dateFormatter = $date_formatter;
+ $this->dateFormatStorage = $date_format_storage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $plugin_id,
+ $plugin_definition,
+ $configuration['field_definition'],
+ $configuration['settings'],
+ $configuration['label'],
+ $configuration['view_mode'],
+ $configuration['third_party_settings'],
+ $container->get('date.formatter'),
+ $container->get('entity.manager')->getStorage('date_format')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function defaultSettings() {
+ return array(
+ 'separator' => '-',
+ 'timezone_override' => '',
+ ) + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ $form = parent::settingsForm($form, $form_state);
+
+ $form['separator'] = array(
+ '#type' => 'textfield',
+ '#title' => $this->t('Date separator'),
+ '#description' => $this->t('The string to separate the start and end dates'),
+ '#default_value' => $this->getSetting('separator'),
+ );
+
+ $form['timezone_override'] = array(
+ '#type' => 'select',
+ '#title' => $this->t('Time zone override'),
+ '#description' => $this->t('The time zone selected here will always be used'),
+ '#options' => system_time_zones(TRUE),
+ '#default_value' => $this->getSetting('timezone_override'),
+ );
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsSummary() {
+ $summary = parent::settingsSummary();
+
+ if ($separator = $this->getSetting('separator')) {
+ $summary[] = $this->t('Separator: %separator', array('%separator' => $separator));
+ }
+
+ if ($override = $this->getSetting('timezone_override')) {
+ $summary[] = $this->t('Time zone: @timezone', array('@timezone' => $override));
+ }
+
+ return $summary;
+ }
+
+ /**
+ * Creates a formatted date as a string.
+ *
+ * @param \Drupal\Core\Datetime\DrupalDateTime $date
+ * The date.
+ *
+ * @return string
+ * A formatted date range string using the chosen format.
+ */
+ abstract protected function formatDate($date);
+
+ /**
+ * Sets the proper time zone on a DrupalDateTime object for the current user.
+ *
+ * A DrupalDateTime object loaded from the database will have the UTC time
+ * zone applied to it. This method will apply the time zone for the current
+ * user, based on system and user settings.
+ *
+ * @see drupal_get_user_timezone()
+ *
+ * @param \Drupal\Core\Datetime\DrupalDateTime $date
+ * A DrupalDateTime object.
+ */
+ protected function setTimeZone(DrupalDateTime $date) {
+ $date->setTimeZone(timezone_open(drupal_get_user_timezone()));
+ }
+
+ /**
+ * Gets a settings array suitable for DrupalDateTime::format().
+ *
+ * @return array
+ * The settings array that can be passed to DrupalDateTime::format().
+ */
+ protected function getFormatSettings() {
+ $settings = [];
+
+ if ($this->getSetting('timezone_override') != '') {
+ $settings['timezone'] = $this->getSetting('timezone_override');
+ }
+
+ return $settings;
+ }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php
new file mode 100644
index 0000000..a548e05
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php
@@ -0,0 +1,66 @@
+getSetting('separator');
+
+ foreach ($items as $delta => $item) {
+ if ($item->start_date && $item->end_date) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+ $start_date = $item->start_date;
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+ $end_date = $item->end_date;
+
+ if ($this->getFieldSetting('daterange_type') == 'date') {
+ // A date without time will pick up the current time, use the default.
+ datetime_date_default_time($start_date);
+ datetime_date_default_time($end_date);
+ }
+
+ $this->setTimeZone($start_date);
+ $this->setTimeZone($end_date);
+
+ $elements[$delta] = [
+ '#cache' => [
+ 'contexts' => [
+ 'timezone',
+ ],
+ ],
+ '#plain_text' => $this->formatDate($start_date) . ' ' . $separator . ' ' . $this->formatDate($end_date),
+ ];
+ }
+ }
+
+ return $elements;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function formatDate($date) {
+ $format = $this->getFieldSetting('daterange_type') == 'date' ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT;
+ $timezone = $this->getSetting('timezone_override');
+ return $this->dateFormatter->format($date->getTimestamp(), 'custom', $format, $timezone != '' ? $timezone : NULL);
+ }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeFieldItemList.php b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeFieldItemList.php
new file mode 100644
index 0000000..1f9c6bb
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeFieldItemList.php
@@ -0,0 +1,115 @@
+getFieldDefinition()->getDefaultValueCallback())) {
+ $default_value = $this->getFieldDefinition()->getDefaultValueLiteral();
+
+ $element = array(
+ '#parents' => array('default_value_input'),
+ 'default_date_type' => array(
+ '#type' => 'select',
+ '#title' => t('Default dates'),
+ '#description' => t('Set a default value for these dates.'),
+ '#default_value' => isset($default_value[0]['default_date_type']) ? $default_value[0]['default_date_type'] : '',
+ '#options' => array(
+ static::DEFAULT_VALUE_NOW => t('Current date'),
+ static::DEFAULT_VALUE_CUSTOM => t('Relative date'),
+ ),
+ '#empty_value' => '',
+ ),
+ 'default_date' => array(
+ '#type' => 'textfield',
+ '#title' => t('Relative default value'),
+ '#description' => t("Describe a time by reference to the current day, like '+90 days' (90 days from the day the field is created) or '+1 Saturday' (the next Saturday). See strtotime for more details."),
+ '#default_value' => (isset($default_value[0]['default_date_type']) && $default_value[0]['default_date_type'] == static::DEFAULT_VALUE_CUSTOM) ? $default_value[0]['default_date'] : '',
+ '#states' => array(
+ 'visible' => array(
+ ':input[id="edit-default-value-input-default-date-type"]' => array('value' => static::DEFAULT_VALUE_CUSTOM),
+ )
+ )
+ )
+ );
+
+ return $element;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultValuesFormValidate(array $element, array &$form, FormStateInterface $form_state) {
+ if ($form_state->getValue(['default_value_input', 'default_date_type']) == static::DEFAULT_VALUE_CUSTOM) {
+ $is_strtotime = @strtotime($form_state->getValue(array('default_value_input', 'default_date')));
+ if (!$is_strtotime) {
+ $form_state->setErrorByName('default_value_input][default_date', t('The relative date value entered is invalid.'));
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultValuesFormSubmit(array $element, array &$form, FormStateInterface $form_state) {
+ if ($form_state->getValue(array('default_value_input', 'default_date_type'))) {
+ if ($form_state->getValue(array('default_value_input', 'default_date_type')) == static::DEFAULT_VALUE_NOW) {
+ $form_state->setValueForElement($element['default_date'], static::DEFAULT_VALUE_NOW);
+ }
+ return array($form_state->getValue('default_value_input'));
+ }
+ return array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function processDefaultValue($default_value, FieldableEntityInterface $entity, FieldDefinitionInterface $definition) {
+ $default_value = parent::processDefaultValue($default_value, $entity, $definition);
+
+ if (isset($default_value[0]['default_date_type'])) {
+ // A default value should be in the format and timezone used for date
+ // storage.
+ $date = new DrupalDateTime($default_value[0]['default_date'], DATETIME_STORAGE_TIMEZONE);
+ $storage_format = $definition->getSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE ? DATERANGE_DATE_STORAGE_FORMAT : DATERANGE_DATETIME_STORAGE_FORMAT;
+ $value = $date->format($storage_format);
+ // We only provide a default value for the first item, as do all fields.
+ // Otherwise, there is no way to clear out unwanted values on multiple
+ // value fields.
+ $default_value = array(
+ array(
+ 'start_value' => $value,
+ 'start_date' => $date,
+ 'end_value' => $value,
+ 'end_date' => $date,
+ )
+ );
+ }
+ return $default_value;
+ }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeItem.php b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeItem.php
new file mode 100644
index 0000000..6aca3b2
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeItem.php
@@ -0,0 +1,162 @@
+ 'daterange',
+ ) + parent::defaultStorageSettings();
+ }
+
+ /**
+ * Value for the 'daterange_type' setting: store only a date.
+ */
+ const DATERANGE_TYPE_DATE = 'date';
+
+ /**
+ * Value for the 'daterange_type' setting: store a date and time.
+ */
+ const DATERANGE_TYPE_DATETIME = 'datetime';
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+ $properties['start_value'] = DataDefinition::create('datetime_iso8601')
+ ->setLabel(t('Start date value'))
+ ->setRequired(TRUE);
+
+ $properties['start_date'] = DataDefinition::create('any')
+ ->setLabel(t('Computed start date'))
+ ->setDescription(t('The computed start DateTime object.'))
+ ->setComputed(TRUE)
+ ->setClass('\Drupal\datetime\DateTimeComputed')
+ ->setSetting('date source', 'start_value');
+
+ $properties['end_value'] = DataDefinition::create('datetime_iso8601')
+ ->setLabel(t('End date value'))
+ ->setRequired(TRUE);
+
+ $properties['end_date'] = DataDefinition::create('any')
+ ->setLabel(t('Computed end date'))
+ ->setDescription(t('The computed end DateTime object.'))
+ ->setComputed(TRUE)
+ ->setClass('\Drupal\datetime\DateTimeComputed')
+ ->setSetting('date source', 'end_value');
+
+ return $properties;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function schema(FieldStorageDefinitionInterface $field_definition) {
+ return array(
+ 'columns' => array(
+ 'start_value' => array(
+ 'description' => 'The start date value.',
+ 'type' => 'varchar',
+ 'length' => 20,
+ ),
+ 'end_value' => array(
+ 'description' => 'The end date value.',
+ 'type' => 'varchar',
+ 'length' => 20,
+ ),
+ ),
+ 'indexes' => array(
+ 'start_value' => array('start_value'),
+ 'end_value' => array('end_value'),
+ ),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
+ $element = array();
+
+ $element['datetime_type'] = array(
+ '#type' => 'select',
+ '#title' => $this->t('Date type'),
+ '#description' => $this->t('Choose the type of date to create.'),
+ '#default_value' => $this->getSetting('datetime_type'),
+ '#options' => array(
+ static::DATERANGE_TYPE_DATETIME => t('Date and time'),
+ static::DATERANGE_TYPE_DATE => t('Date only'),
+ ),
+ '#disabled' => $has_data,
+ );
+
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
+ $type = $field_definition->getSetting('daterange_type');
+
+ // Just pick a date in the past year. No guidance is provided by this Field
+ // type.
+ $start = REQUEST_TIME - mt_rand(0, 86400 * 365) - 86400;
+ $end = $start + 86400;
+ if ($type == static::DATERANGE_TYPE_DATE) {
+ $values['start_value'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $start);
+ $values['end_value'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $end);
+ }
+ else {
+ $values['start_value'] = gmdate(DATETIME_DATETIME_STORAGE_FORMAT, $start);
+ $values['end_value'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $end);
+ }
+ return $values;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isEmpty() {
+ $start_value = $this->get('start_value')->getValue();
+ $end_value = $this->get('end_value')->getValue();
+ return ($start_value === NULL || $start_value === '') && ($end_value === NULL || $end_value === '');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onChange($property_name, $notify = TRUE) {
+ // Enforce that the computed date is recalculated.
+ if ($property_name == 'start_value') {
+ $this->start_date = NULL;
+ }
+ elseif ($property_name == 'end_value') {
+ $this->end_date = NULL;
+ }
+ parent::onChange($property_name, $notify);
+ }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php
new file mode 100644
index 0000000..ffc54f5
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php
@@ -0,0 +1,155 @@
+ '15',
+ 'date_order' => 'YMD',
+ 'time_type' => '24',
+ ) + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+ $element = parent::formElement($items, $delta, $element, $form, $form_state);
+
+ $date_order = $this->getSetting('date_order');
+
+ if ($this->getFieldSetting('daterange_type') == 'datetime') {
+ $time_type = $this->getSetting('time_type');
+ $increment = $this->getSetting('increment');
+ }
+ else {
+ $time_type = '';
+ $increment = '';
+ }
+
+ // Set up the date part order array.
+ switch ($date_order) {
+ case 'YMD':
+ $date_part_order = array('year', 'month', 'day');
+ break;
+
+ case 'MDY':
+ $date_part_order = array('month', 'day', 'year');
+ break;
+
+ case 'DMY':
+ $date_part_order = array('day', 'month', 'year');
+ break;
+ }
+ switch ($time_type) {
+ case '24':
+ $date_part_order = array_merge($date_part_order, array('hour', 'minute'));
+ break;
+
+ case '12':
+ $date_part_order = array_merge($date_part_order, array('hour', 'minute', 'ampm'));
+ break;
+
+ case 'none':
+ break;
+ }
+
+ $element['start_value'] = array(
+ '#type' => 'datelist',
+ '#date_increment' => $increment,
+ '#date_part_order' => $date_part_order,
+ ) + $element['start_value'];
+
+ $element['end_value'] = array(
+ '#type' => 'datelist',
+ '#date_increment' => $increment,
+ '#date_part_order' => $date_part_order,
+ ) + $element['end_value'];
+
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function settingsForm(array $form, FormStateInterface $form_state) {
+ $element = parent::settingsForm($form, $form_state);
+
+ $element['date_order'] = array(
+ '#type' => 'select',
+ '#title' => t('Date part order'),
+ '#default_value' => $this->getSetting('date_order'),
+ '#options' => array('MDY' => t('Month/Day/Year'), 'DMY' => t('Day/Month/Year'), 'YMD' => t('Year/Month/Day')),
+ );
+
+ if ($this->getFieldSetting('daterange_type') == 'datetime') {
+ $element['time_type'] = array(
+ '#type' => 'select',
+ '#title' => t('Time type'),
+ '#default_value' => $this->getSetting('time_type'),
+ '#options' => array('24' => t('24 hour time'), '12' => t('12 hour time')),
+ );
+
+ $element['increment'] = [
+ '#type' => 'select',
+ '#title' => t('Time increments'),
+ '#default_value' => $this->getSetting('increment'),
+ '#options' => [
+ 1 => t('1 minute'),
+ 5 => t('5 minute'),
+ 10 => t('10 minute'),
+ 15 => t('15 minute'),
+ 30 => t('30 minute'),
+ ],
+ ];
+ }
+ else {
+ $element['time_type'] = array(
+ '#type' => 'hidden',
+ '#value' => 'none',
+ );
+
+ $element['increment'] = [
+ '#type' => 'hidden',
+ '#value' => $this->getSetting('increment'),
+ ];
+ }
+
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsSummary() {
+ $summary = array();
+
+ $summary[] = t('Date part order: @order', array('@order' => $this->getSetting('date_order')));
+ if ($this->getFieldSetting('daterange_type') == 'datetime') {
+ $summary[] = t('Time type: @time_type', array('@time_type' => $this->getSetting('time_type')));
+ $summary[] = t('Time increments: @increment', array('@increment' => $this->getSetting('increment')));
+ }
+
+ return $summary;
+ }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php
new file mode 100644
index 0000000..e304b1f
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php
@@ -0,0 +1,102 @@
+dateStorage = $date_storage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $plugin_id,
+ $plugin_definition,
+ $configuration['field_definition'],
+ $configuration['settings'],
+ $configuration['third_party_settings'],
+ $container->get('entity.manager')->getStorage('date_format')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+ $element = parent::formElement($items, $delta, $element, $form, $form_state);
+
+ debug([$this->getSettings()]);
+
+ // Identify the type of date and time elements to use.
+ switch ($this->getFieldSetting('daterange_type')) {
+ case DateRangeItem::DATERANGE_TYPE_DATE:
+ $date_type = 'date';
+ $time_type = 'none';
+ $date_format = $this->dateStorage->load('html_date')->getPattern();
+ $time_format = '';
+ break;
+
+ default:
+ $date_type = 'date';
+ $time_type = 'time';
+ $date_format = $this->dateStorage->load('html_date')->getPattern();
+ $time_format = $this->dateStorage->load('html_time')->getPattern();
+ break;
+ }
+
+ $element['start_value'] += array(
+ '#date_date_format' => $date_format,
+ '#date_date_element' => $date_type,
+ '#date_date_callbacks' => array(),
+ '#date_time_format' => $time_format,
+ '#date_time_element' => $time_type,
+ '#date_time_callbacks' => array(),
+ );
+
+ $element['end_value'] += array(
+ '#date_date_format' => $date_format,
+ '#date_date_element' => $date_type,
+ '#date_date_callbacks' => array(),
+ '#date_time_format' => $time_format,
+ '#date_time_element' => $time_type,
+ '#date_time_callbacks' => array(),
+ );
+
+ return $element;
+ }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php
new file mode 100644
index 0000000..28c7e12
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php
@@ -0,0 +1,117 @@
+ $this->t('Start'),
+ '#type' => 'datetime',
+ '#default_value' => NULL,
+ '#date_increment' => 1,
+ '#date_timezone' => drupal_get_user_timezone(),
+ '#required' => $element['#required'],
+ );
+
+ $element['end_value'] = array(
+ '#title' => $this->t('End'),
+ '#type' => 'datetime',
+ '#default_value' => NULL,
+ '#date_increment' => 1,
+ '#date_timezone' => drupal_get_user_timezone(),
+ '#required' => $element['#required'],
+ );
+
+ if ($items[$delta]->start_date) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+ $start_date = $items[$delta]->start_date;
+ // The date was created and verified during field_load(), so it is safe to
+ // use without further inspection.
+ if ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE) {
+ // A date without time will pick up the current time, use the default
+ // time.
+ datetime_date_default_time($start_date);
+ }
+ $start_date->setTimezone(new \DateTimeZone($element['start_value']['#date_timezone']));
+ $element['start_value']['#default_value'] = $start_date;
+
+ if ($items[$delta]->end_date) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+ $end_date = $items[$delta]->end_date;
+ // The date was created and verified during field_load(), so it is safe to
+ // use without further inspection.
+ if ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE) {
+ // A date without time will pick up the current time, use the default
+ // time.
+ datetime_date_default_time($end_date);
+ }
+ $end_date->setTimezone(new \DateTimeZone($element['end_value']['#date_timezone']));
+ $element['end_value']['#default_value'] = $end_date;
+ }
+ }
+
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
+ // The widget form element type has transformed the value to a
+ // DrupalDateTime object at this point. We need to convert it back to the
+ // storage timezone and format.
+ foreach ($values as &$item) {
+ if (!empty($item['start_value']) && $item['start_value'] instanceof DrupalDateTime) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+ $start_date = $item['start_value'];
+ switch ($this->getFieldSetting('daterange_type')) {
+ case DateRangeItem::DATERANGE_TYPE_DATE:
+ // If this is a date-only field, set it to the default time so the
+ // timezone conversion can be reversed.
+ datetime_date_default_time($start_date);
+ $format = DATETIME_DATE_STORAGE_FORMAT;
+ break;
+
+ default:
+ $format = DATETIME_DATETIME_STORAGE_FORMAT;
+ break;
+ }
+ // Adjust the date for storage.
+ $start_date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
+ $item['start_value'] = $start_date->format($format);
+
+ if (!empty($item['end_value']) && $item['end_value'] instanceof DrupalDateTime) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+ $end_date = $item['end_value'];
+ // Adjust the date for storage.
+ $end_date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
+ $item['end_value'] = $end_date->format($format);
+ }
+ }
+ }
+ return $values;
+ }
+
+}
diff --git a/core/modules/datetime/src/Tests/DateRangeFieldTest.php b/core/modules/datetime/src/Tests/DateRangeFieldTest.php
new file mode 100644
index 0000000..5ef4c71
--- /dev/null
+++ b/core/modules/datetime/src/Tests/DateRangeFieldTest.php
@@ -0,0 +1,155 @@
+config('system.date')
+ ->set('timezone.user.configurable', 0)
+ ->set('timezone.default', 'Asia/Tokyo')
+ ->save();
+
+ $web_user = $this->drupalCreateUser(array(
+ 'access content',
+ 'view test entity',
+ 'administer entity_test content',
+ 'administer entity_test form display',
+ 'administer content types',
+ 'administer node fields',
+ ));
+ $this->drupalLogin($web_user);
+
+ // Create a field with settings to validate.
+ $field_name = Unicode::strtolower($this->randomMachineName());
+ $this->fieldStorage = FieldStorageConfig::create(array(
+ 'field_name' => $field_name,
+ 'entity_type' => 'entity_test',
+ 'type' => 'daterange',
+ 'settings' => array('daterange_type' => 'date'),
+ ));
+ $this->fieldStorage->save();
+ $this->field = FieldConfig::create([
+ 'field_storage' => $this->fieldStorage,
+ 'bundle' => 'entity_test',
+ 'required' => TRUE,
+ ]);
+ $this->field->save();
+
+ entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+ ->setComponent($field_name, array(
+ 'type' => 'daterange_default',
+ ))
+ ->save();
+
+ $this->defaultSettings = array(
+ 'separator' => '-',
+ 'timezone_override' => '',
+ );
+
+ $this->displayOptions = array(
+ 'type' => 'daterange_default',
+ 'label' => 'hidden',
+ 'settings' => array('format_type' => 'medium') + $this->defaultSettings,
+ );
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ }
+
+ /**
+ * Tests date field functionality.
+ */
+ function testDateRangeField() {
+ $this->fail('Write a real test.');
+ }
+
+ /**
+ * Tests date and time field.
+ */
+ function testDatetimeRangeField() {
+ $this->fail('Write a real test.');
+ }
+
+ /**
+ * Tests Date Range List Widget functionality.
+ */
+ function testDatelistWidget() {
+ $this->fail('Write a real test.');
+ }
+
+ /**
+ * Test default value functionality.
+ */
+ function testDefaultValue() {
+ $this->fail('Write a real test.');
+ }
+
+ /**
+ * Test that invalid values are caught and marked as invalid.
+ */
+ function testInvalidField() {
+ $this->fail('Write a real test.');
+ }
+
+ /**
+ * Tests that 'Date' field storage setting form is disabled if field has data.
+ */
+ public function testDateStorageSettings() {
+ $this->fail('Write a real test.');
+ }
+
+}