diff --git a/core/modules/datetime/config/schema/datetime.schema.yml b/core/modules/datetime/config/schema/datetime.schema.yml
index 406a2fd..e627ce0 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:
+ daterange_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/datetime.views.inc b/core/modules/datetime/datetime.views.inc
index d3b0d18..b1742cf 100644
--- a/core/modules/datetime/datetime.views.inc
+++ b/core/modules/datetime/datetime.views.inc
@@ -11,42 +11,44 @@
* Implements hook_field_views_data().
*/
function datetime_field_views_data(FieldStorageConfigInterface $field_storage) {
- // @todo This code only covers configurable fields, handle base table fields
- // in https://www.drupal.org/node/2489476.
- $data = views_field_default_views_data($field_storage);
- foreach ($data as $table_name => $table_data) {
- // Set the 'datetime' filter type.
- $data[$table_name][$field_storage->getName() . '_value']['filter']['id'] = 'datetime';
+ if ($field_storage->getType() == 'datetime') {
+ // @todo This code only covers configurable fields, handle base table fields
+ // in https://www.drupal.org/node/2489476.
+ $data = views_field_default_views_data($field_storage);
+ foreach ($data as $table_name => $table_data) {
+ // Set the 'datetime' filter type.
+ $data[$table_name][$field_storage->getName() . '_value']['filter']['id'] = 'datetime';
- // Set the 'datetime' argument type.
- $data[$table_name][$field_storage->getName() . '_value']['argument']['id'] = 'datetime';
+ // Set the 'datetime' argument type.
+ $data[$table_name][$field_storage->getName() . '_value']['argument']['id'] = 'datetime';
- // Create year, month, and day arguments.
- $group = $data[$table_name][$field_storage->getName() . '_value']['group'];
- $arguments = [
- // Argument type => help text.
- 'year' => t('Date in the form of YYYY.'),
- 'month' => t('Date in the form of MM (01 - 12).'),
- 'day' => t('Date in the form of DD (01 - 31).'),
- 'week' => t('Date in the form of WW (01 - 53).'),
- 'year_month' => t('Date in the form of YYYYMM.'),
- 'full_date' => t('Date in the form of CCYYMMDD.'),
- ];
- foreach ($arguments as $argument_type => $help_text) {
- $data[$table_name][$field_storage->getName() . '_value_' . $argument_type] = [
- 'title' => $field_storage->getLabel() . ' (' . $argument_type . ')',
- 'help' => $help_text,
- 'argument' => [
- 'field' => $field_storage->getName() . '_value',
- 'id' => 'datetime_' . $argument_type,
- ],
- 'group' => $group,
+ // Create year, month, and day arguments.
+ $group = $data[$table_name][$field_storage->getName() . '_value']['group'];
+ $arguments = [
+ // Argument type => help text.
+ 'year' => t('Date in the form of YYYY.'),
+ 'month' => t('Date in the form of MM (01 - 12).'),
+ 'day' => t('Date in the form of DD (01 - 31).'),
+ 'week' => t('Date in the form of WW (01 - 53).'),
+ 'year_month' => t('Date in the form of YYYYMM.'),
+ 'full_date' => t('Date in the form of CCYYMMDD.'),
];
+ foreach ($arguments as $argument_type => $help_text) {
+ $data[$table_name][$field_storage->getName() . '_value_' . $argument_type] = [
+ 'title' => $field_storage->getLabel() . ' (' . $argument_type . ')',
+ 'help' => $help_text,
+ 'argument' => [
+ 'field' => $field_storage->getName() . '_value',
+ 'id' => 'datetime_' . $argument_type,
+ ],
+ 'group' => $group,
+ ];
+ }
+
+ // Set the 'datetime' sort handler.
+ $data[$table_name][$field_storage->getName() . '_value']['sort']['id'] = 'datetime';
}
- // Set the 'datetime' sort handler.
- $data[$table_name][$field_storage->getName() . '_value']['sort']['id'] = 'datetime';
+ return $data;
}
-
- return $data;
}
diff --git a/core/modules/datetime/src/DateTimeComputed.php b/core/modules/datetime/src/DateTimeComputed.php
index 6939994..49f0512 100644
--- a/core/modules/datetime/src/DateTimeComputed.php
+++ b/core/modules/datetime/src/DateTimeComputed.php
@@ -40,10 +40,12 @@ public function getValue($langcode = NULL) {
return $this->date;
}
+ /** @var \Drupal\Core\Field\FieldItemBase $item */
$item = $this->getParent();
$value = $item->{($this->definition->getSetting('date source'))};
+ $type = $item->getFieldDefinition()->getType();
- $storage_format = $item->getFieldDefinition()->getSetting('datetime_type') == 'date' ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT;
+ $storage_format = $item->getFieldDefinition()->getSetting($type . '_type') == 'date' ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT;
try {
$date = DrupalDateTime::createFromFormat($storage_format, $value, DATETIME_STORAGE_TIMEZONE);
if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
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..ea81d19
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php
@@ -0,0 +1,115 @@
+ 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);
+
+ $start = $this->formatDate($start_date);
+ $end = $this->formatDate($end_date);
+ if ($start !== $end) {
+ $output = $this->formatDate($start_date) . ' ' . $separator . ' ' . $this->formatDate($end_date);
+ }
+ else {
+ $output = $start;
+ }
+
+ $elements[$delta] = [
+ '#cache' => [
+ 'contexts' => [
+ 'timezone',
+ ],
+ ],
+ '#plain_text' => $output,
+ ];
+ }
+ }
+
+ 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'] = [
+ '#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', ['@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..4ea396e
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php
@@ -0,0 +1,159 @@
+ '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',
+ ],
+ ],
+ ];
+
+ $start = $this->formatDate($start_date);
+ $end = $this->formatDate($end_date);
+
+ if ($start !== $end) {
+ $elements[$delta][] = [
+ '#theme' => 'time',
+ '#text' => $start,
+ '#html' => FALSE,
+ '#attributes' => [
+ 'datetime' => $start_iso_date,
+ ]
+ ];
+ $elements[$delta][] = ['#plain_text' => ' ' . $separator . ' '];
+ $elements[$delta][] = [
+ '#theme' => 'time',
+ '#text' => $end,
+ '#html' => FALSE,
+ '#attributes' => [
+ 'datetime' => $end_iso_date,
+ ]
+ ];
+ }
+ else {
+ $elements[$delta][] = [
+ '#theme' => 'time',
+ '#text' => $start,
+ '#html' => FALSE,
+ '#attributes' => [
+ 'datetime' => $start_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'] = [
+ '#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', ['@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..9db9053
--- /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 [
+ 'separator' => '-',
+ 'timezone_override' => '',
+ ] + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ $form = parent::settingsForm($form, $form_state);
+
+ $form['separator'] = [
+ '#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'] = [
+ '#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', ['%separator' => $separator]);
+ }
+
+ if ($override = $this->getSetting('timezone_override')) {
+ $summary[] = $this->t('Time zone: @timezone', ['@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..0fd76fc
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php
@@ -0,0 +1,75 @@
+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);
+
+ $start = $this->formatDate($start_date);
+ $end = $this->formatDate($end_date);
+ if ($start !== $end) {
+ $output = $this->formatDate($start_date) . ' ' . $separator . ' ' . $this->formatDate($end_date);
+ }
+ else {
+ $output = $start;
+ }
+
+ $elements[$delta] = [
+ '#cache' => [
+ 'contexts' => [
+ 'timezone',
+ ],
+ ],
+ '#plain_text' => $output,
+ ];
+ }
+ }
+
+ 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..2c1409d
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeFieldItemList.php
@@ -0,0 +1,115 @@
+getFieldDefinition()->getDefaultValueCallback())) {
+ $default_value = $this->getFieldDefinition()->getDefaultValueLiteral();
+
+ $element = [
+ '#parents' => ['default_value_input'],
+ 'default_date_type' => [
+ '#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' => [
+ static::DEFAULT_VALUE_NOW => t('Current date'),
+ static::DEFAULT_VALUE_CUSTOM => t('Relative date'),
+ ],
+ '#empty_value' => '',
+ ],
+ 'default_date' => [
+ '#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' => [
+ 'visible' => [
+ ':input[id="edit-default-value-input-default-date-type"]' => ['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(['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(['default_value_input', 'default_date_type'])) {
+ if ($form_state->getValue(['default_value_input', 'default_date_type']) == static::DEFAULT_VALUE_NOW) {
+ $form_state->setValueForElement($element['default_date'], static::DEFAULT_VALUE_NOW);
+ }
+ return [$form_state->getValue('default_value_input')];
+ }
+ return [];
+ }
+
+ /**
+ * {@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. All-day ranges are stored the same as date+time ranges.
+ $date = new DrupalDateTime($default_value[0]['default_date'], DATETIME_STORAGE_TIMEZONE);
+ $storage_format = $definition->getSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_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 = [
+ [
+ 'value' => $value,
+ 'start_date' => $date,
+ 'value2' => $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..78e2cb8
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeItem.php
@@ -0,0 +1,172 @@
+ 'datetime',
+ ] + 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';
+
+ /**
+ * Value for the 'daterange_type' setting: store a date and time.
+ */
+ const DATERANGE_TYPE_ALLDAY = 'allday';
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+ $properties['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', 'value');
+
+ $properties['value2'] = 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', 'value2');
+
+ return $properties;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function schema(FieldStorageDefinitionInterface $field_definition) {
+ return [
+ 'columns' => [
+ 'value' => [
+ 'description' => 'The start date value.',
+ 'type' => 'varchar',
+ 'length' => 20,
+ ],
+ 'value2' => [
+ 'description' => 'The end date value.',
+ 'type' => 'varchar',
+ 'length' => 20,
+ ],
+ ],
+ 'indexes' => [
+ 'value' => ['value'],
+ 'value2' => ['value2'],
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
+ $element = [];
+
+ $element['daterange_type'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Date type'),
+ '#description' => $this->t('Choose the type of date to create.'),
+ '#default_value' => $this->getSetting('daterange_type'),
+ '#options' => [
+ static::DATERANGE_TYPE_DATETIME => t('Date and time'),
+ static::DATERANGE_TYPE_DATE => t('Date only'),
+ static::DATERANGE_TYPE_ALLDAY => t('All Day'),
+ ],
+ '#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['value'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $start);
+ $values['value2'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $end);
+ }
+ elseif ($type == static::DATERANGE_TYPE_ALLDAY) {
+ $values['value'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $start);
+ $values['value2'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $end);
+ }
+ else {
+ $values['value'] = gmdate(DATETIME_DATETIME_STORAGE_FORMAT, $start);
+ $values['value2'] = gmdate(DATETIME_DATETIME_STORAGE_FORMAT, $end);
+ }
+ return $values;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isEmpty() {
+ $start_value = $this->get('value')->getValue();
+ $end_value = $this->get('value2')->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 == 'value') {
+ $this->start_date = NULL;
+ }
+ elseif ($property_name == 'value2') {
+ $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..1693b61
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php
@@ -0,0 +1,157 @@
+ '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) {
+ default:
+ 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) {
+ default:
+ 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['value'] = [
+ '#type' => 'datelist',
+ '#date_increment' => $increment,
+ '#date_part_order' => $date_part_order,
+ ] + $element['value'];
+
+ $element['value2'] = [
+ '#type' => 'datelist',
+ '#date_increment' => $increment,
+ '#date_part_order' => $date_part_order,
+ ] + $element['value2'];
+
+ 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..24af1ce
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php
@@ -0,0 +1,107 @@
+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);
+
+ // 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;
+
+ case DateRangeItem::DATERANGE_TYPE_ALLDAY:
+ $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['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['value2'] += 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..418f7f5
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php
@@ -0,0 +1,165 @@
+ $this->t('Start'),
+ '#type' => 'datetime',
+ '#default_value' => NULL,
+ '#date_increment' => 1,
+ '#date_timezone' => drupal_get_user_timezone(),
+ '#required' => $element['#required'],
+ );
+
+ $element['value2'] = 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['value']['#date_timezone']));
+ $element['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['value2']['#date_timezone']));
+ $element['value2']['#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['value']) && $item['value'] instanceof DrupalDateTime) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+ $start_date = $item['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;
+
+ case DateRangeItem::DATERANGE_TYPE_ALLDAY:
+ // All day field start at midnight on the starting date, but are
+ // stored like datetime fields, so we need to adjust the time.
+ $start_date->setTime(0, 0, 0);
+ $format = DATETIME_DATETIME_STORAGE_FORMAT;
+ break;
+
+ default:
+ $format = DATETIME_DATETIME_STORAGE_FORMAT;
+ break;
+ }
+ // Adjust the date for storage.
+ $start_date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
+ $item['value'] = $start_date->format($format);
+ }
+
+ if (!empty($item['value2']) && $item['value2'] instanceof DrupalDateTime) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+ $end_date = $item['value2'];
+ 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($end_date);
+ $format = DATETIME_DATE_STORAGE_FORMAT;
+ break;
+
+ case DateRangeItem::DATERANGE_TYPE_ALLDAY:
+ // All day field end at midnight on the end date, but are
+ // stored like datetime fields, so we need to adjust the time.
+ $end_date->setTime(23, 59, 59);
+ $format = DATETIME_DATETIME_STORAGE_FORMAT;
+ break;
+
+ default:
+ $format = DATETIME_DATETIME_STORAGE_FORMAT;
+ break;
+ }
+ // Adjust the date for storage.
+ $end_date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
+ $item['value2'] = $end_date->format($format);
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * Validates that the start <= the end date.
+ */
+ public function validateStartEnd($element, FormStateInterface $form_state) {
+ $start_date = $element['value']['#value']['object'];
+ $end_date = $element['value2']['#value']['object'];
+
+ if ($start_date instanceof DrupalDateTime && $end_date instanceof DrupalDateTime) {
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+ /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+
+ if ($start_date->format('U') !== $end_date->format('U')) {
+ $interval = $start_date->diff($end_date);
+ if ($interval->invert === 1) {
+ $form_state->setError($element, $this->t('Start date should be equal to, or before, end date'));
+ }
+ }
+ }
+ }
+
+}
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.');
+ }
+
+}