diff --git a/core/modules/datetime/config/schema/datetime.schema.yml b/core/modules/datetime/config/schema/datetime.schema.yml
index 406a2fd..200751a 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,84 @@ 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_start_date_type:
+ type: string
+ label: 'Default start date type'
+ default_start_date:
+ type: string
+ label: 'Default start date value'
+ default_end_date_type:
+ type: string
+ label: 'Default end date type'
+ default_end_date:
+ type: string
+ label: 'Default end 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/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..d6dbf7d
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeFieldItemList.php
@@ -0,0 +1,149 @@
+getFieldDefinition()->getDefaultValueCallback())) {
+ $default_value = $this->getFieldDefinition()->getDefaultValueLiteral();
+
+ $element = [
+ '#parents' => ['default_value_input'],
+ 'default_start_date_type' => [
+ '#type' => 'select',
+ '#title' => $this->t('Default start date'),
+ '#description' => $this->t('Set a default value for the start date.'),
+ '#default_value' => isset($default_value[0]['default_start_date_type']) ? $default_value[0]['default_start_date_type'] : '',
+ '#options' => [
+ static::DEFAULT_VALUE_NOW => $this->t('Current date'),
+ static::DEFAULT_VALUE_CUSTOM => $this->t('Relative date'),
+ ],
+ '#empty_value' => '',
+ ],
+ 'default_start_date' => [
+ '#type' => 'textfield',
+ '#title' => $this->t('Relative default value'),
+ '#description' => $this->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_start_date_type']) && $default_value[0]['default_start_date_type'] == static::DEFAULT_VALUE_CUSTOM) ? $default_value[0]['default_start_date'] : '',
+ '#states' => [
+ 'visible' => [
+ ':input[id="edit-default-value-input-default-start-date-type"]' => ['value' => static::DEFAULT_VALUE_CUSTOM],
+ ],
+ ],
+ ],
+ 'default_end_date_type' => [
+ '#type' => 'select',
+ '#title' => $this->t('Default end date'),
+ '#description' => $this->t('Set a default value for the end date.'),
+ '#default_value' => isset($default_value[0]['default_end_date_type']) ? $default_value[0]['default_end_date_type'] : '',
+ '#options' => [
+ static::DEFAULT_VALUE_NOW => $this->t('Current date'),
+ static::DEFAULT_VALUE_CUSTOM => $this->t('Relative date'),
+ ],
+ '#empty_value' => '',
+ ],
+ 'default_end_date' => [
+ '#type' => 'textfield',
+ '#title' => $this->t('Relative default value'),
+ '#description' => $this->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_end_date_type']) && $default_value[0]['default_end_date_type'] == static::DEFAULT_VALUE_CUSTOM) ? $default_value[0]['default_end_date'] : '',
+ '#states' => [
+ 'visible' => [
+ ':input[id="edit-default-value-input-default-end-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_start_date_type']) == static::DEFAULT_VALUE_CUSTOM) {
+ $is_strtotime = @strtotime($form_state->getValue(['default_value_input', 'default_start_date']));
+ if (!$is_strtotime) {
+ $form_state->setErrorByName('default_value_input][default_start_date', $this->t('The relative start date value entered is invalid.'));
+ }
+ }
+
+ if ($form_state->getValue(['default_value_input', 'default_end_date_type']) == static::DEFAULT_VALUE_CUSTOM) {
+ $is_strtotime = @strtotime($form_state->getValue(['default_value_input', 'default_end_date']));
+ if (!$is_strtotime) {
+ $form_state->setErrorByName('default_value_input][default_end_date', $this->t('The relative end date value entered is invalid.'));
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultValuesFormSubmit(array $element, array &$form, FormStateInterface $form_state) {
+ if ($form_state->getValue(['default_value_input', 'default_start_date_type']) && $form_state->getValue(['default_value_input', 'default_end_date_type'])) {
+ if ($form_state->getValue(['default_value_input', 'default_start_date_type']) == static::DEFAULT_VALUE_NOW) {
+ $form_state->setValueForElement($element['default_start_date'], static::DEFAULT_VALUE_NOW);
+ }
+ if ($form_state->getValue(['default_value_input', 'default_end_date_type']) == static::DEFAULT_VALUE_NOW) {
+ $form_state->setValueForElement($element['default_end_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_start_date_type']) && isset($default_value[0]['default_end_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.
+ $storage_format = $definition->getSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT;
+ $start_date = new DrupalDateTime($default_value[0]['default_start_date'], DATETIME_STORAGE_TIMEZONE);
+ $start_value = $start_date->format($storage_format);
+ $end_date = new DrupalDateTime($default_value[0]['default_end_date'], DATETIME_STORAGE_TIMEZONE);
+ $end_value = $end_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' => $start_value,
+ 'start_date' => $start_date,
+ 'value2' => $end_value,
+ 'end_date' => $end_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..a918213
--- /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') == DateRangeItem::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) {
+ 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..08cbc8f
--- /dev/null
+++ b/core/modules/datetime/src/Tests/DateRangeFieldTest.php
@@ -0,0 +1,1137 @@
+config('system.date')
+ ->set('timezone.user.configurable', 0)
+ ->set('timezone.default', 'Asia/Tokyo')
+ ->save();
+
+ $web_user = $this->drupalCreateUser([
+ '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([
+ 'field_name' => $field_name,
+ 'entity_type' => 'entity_test',
+ 'type' => 'daterange',
+ 'settings' => ['daterange_type' => DateRangeItem::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, [
+ 'type' => 'daterange_default',
+ ])
+ ->save();
+
+ $this->defaultSettings = [
+ 'separator' => '-',
+ 'timezone_override' => '',
+ ];
+
+ $this->displayOptions = [
+ 'type' => 'daterange_default',
+ 'label' => 'hidden',
+ 'settings' => ['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() {
+ $field_name = $this->fieldStorage->getName();
+
+ // Ensure field is set to a date-only field.
+ $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_TYPE_DATE);
+ $this->fieldStorage->save();
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+ $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.');
+ $this->assertFieldByName("{$field_name}[0][value2][date]", '', 'End date element found.');
+ $this->assertFieldByXPath('//*[@id="edit-' . $field_name . '-wrapper"]/h4[contains(@class, "js-form-required")]', TRUE, 'Required markup found');
+ $this->assertNoFieldByName("{$field_name}[0][value][time]", '', 'Start time element not found.');
+ $this->assertNoFieldByName("{$field_name}[0][value2][time]", '', 'End time element not found.');
+
+ // Build up dates in the UTC timezone.
+ $value = '2012-12-31 00:00:00';
+ $start_date = new DrupalDateTime($value, 'UTC');
+ $value2 = '2013-06-06 00:00:00';
+ $end_date = new DrupalDateTime($value2, 'UTC');
+
+ // The expected values will use the default time.
+ datetime_date_default_time($start_date);
+ datetime_date_default_time($end_date);
+
+ // Update the timezone to the system default.
+ $start_date->setTimezone(timezone_open(drupal_get_user_timezone()));
+ $end_date->setTimezone(timezone_open(drupal_get_user_timezone()));
+
+ // Submit a valid date and ensure it is accepted.
+ $date_format = DateFormat::load('html_date')->getPattern();
+ $time_format = DateFormat::load('html_time')->getPattern();
+
+ $edit = array(
+ "{$field_name}[0][value][date]" => $start_date->format($date_format),
+ "{$field_name}[0][value2][date]" => $end_date->format($date_format),
+ );
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+ $id = $match[1];
+ $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
+ $this->assertRaw($start_date->format($date_format));
+ $this->assertNoRaw($start_date->format($time_format));
+ $this->assertRaw($end_date->format($date_format));
+ $this->assertNoRaw($end_date->format($time_format));
+
+ // Verify that the default formatter works.
+ $this->displayOptions['settings'] = [
+ 'format_type' => 'long',
+ 'separator' => 'THESEPARATOR',
+ ] + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+
+ $start_expected = format_date($start_date->getTimestamp(), 'long');
+ $start_expected_iso = format_date($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+ $end_expected = format_date($end_date->getTimestamp(), 'long');
+ $end_expected_iso = format_date($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+ $this->renderTestEntity($id);
+ $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, SafeMarkup::format('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $start_expected, '%expected_iso' => $start_expected_iso]));
+ $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, SafeMarkup::format('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $end_expected, '%expected_iso' => $end_expected_iso]));
+ $this->assertText(' THESEPARATOR ', 'Found proper separator');
+
+ // Verify that the plain formatter works.
+ $this->displayOptions['type'] = 'daterange_plain';
+ $this->displayOptions['settings'] = $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format(DATETIME_DATE_STORAGE_FORMAT) . ' - ' . $end_date->format(DATETIME_DATE_STORAGE_FORMAT);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, SafeMarkup::format('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected)));
+
+ // Verify that the custom formatter works.
+ $this->displayOptions['type'] = 'daterange_custom';
+ $this->displayOptions['settings'] = array('date_format' => 'm/d/Y') + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' - ' . $end_date->format($this->displayOptions['settings']['date_format']);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, SafeMarkup::format('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+ }
+
+ /**
+ * Tests date and time field.
+ */
+ function testDatetimeRangeField() {
+ $field_name = $this->fieldStorage->getName();
+
+ // Ensure the field to a datetime field.
+ $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_TYPE_DATETIME);
+ $this->fieldStorage->save();
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+ $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.');
+ $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Start time element found.');
+ $this->assertFieldByName("{$field_name}[0][value2][date]", '', 'End date element found.');
+ $this->assertFieldByName("{$field_name}[0][value2][time]", '', 'End time element found.');
+
+ // Build up dates in the UTC timezone.
+ $value = '2012-12-31 00:00:00';
+ $start_date = new DrupalDateTime($value, 'UTC');
+ $value2 = '2013-06-06 00:00:00';
+ $end_date = new DrupalDateTime($value2, 'UTC');
+
+ // Update the timezone to the system default.
+ $start_date->setTimezone(timezone_open(drupal_get_user_timezone()));
+ $end_date->setTimezone(timezone_open(drupal_get_user_timezone()));
+
+ // Submit a valid date and ensure it is accepted.
+ $date_format = DateFormat::load('html_date')->getPattern();
+ $time_format = DateFormat::load('html_time')->getPattern();
+
+ $edit = array(
+ "{$field_name}[0][value][date]" => $start_date->format($date_format),
+ "{$field_name}[0][value][time]" => $start_date->format($time_format),
+ "{$field_name}[0][value2][date]" => $end_date->format($date_format),
+ "{$field_name}[0][value2][time]" => $end_date->format($time_format),
+ );
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+ $id = $match[1];
+ $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
+ $this->assertRaw($start_date->format($date_format));
+ $this->assertRaw($start_date->format($time_format));
+ $this->assertRaw($end_date->format($date_format));
+ $this->assertRaw($end_date->format($time_format));
+
+ // Verify that the default formatter works.
+ $this->displayOptions['settings'] = [
+ 'format_type' => 'long',
+ 'separator' => 'THESEPARATOR',
+ ] + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+
+ $start_expected = format_date($start_date->getTimestamp(), 'long');
+ $start_expected_iso = format_date($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+ $end_expected = format_date($end_date->getTimestamp(), 'long');
+ $end_expected_iso = format_date($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+ $this->renderTestEntity($id);
+ $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, SafeMarkup::format('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $start_expected, '%expected_iso' => $start_expected_iso]));
+ $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, SafeMarkup::format('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $end_expected, '%expected_iso' => $end_expected_iso]));
+ $this->assertText(' THESEPARATOR ', 'Found proper separator');
+
+ // Verify that the plain formatter works.
+ $this->displayOptions['type'] = 'daterange_plain';
+ $this->displayOptions['settings'] = $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format(DATETIME_DATETIME_STORAGE_FORMAT) . ' - ' . $end_date->format(DATETIME_DATETIME_STORAGE_FORMAT);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, SafeMarkup::format('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected)));
+
+ // Verify that the 'datetime_custom' formatter works.
+ $this->displayOptions['type'] = 'daterange_custom';
+ $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A'] + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' - ' . $end_date->format($this->displayOptions['settings']['date_format']);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, SafeMarkup::format('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+
+ // Verify that the 'timezone_override' setting works.
+ $this->displayOptions['type'] = 'daterange_custom';
+ $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A', 'timezone_override' => 'America/New_York'] + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
+ $expected .= ' - ' . $end_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, SafeMarkup::format('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+ }
+
+ /**
+ * Tests all-day field.
+ */
+ function testAlldayRangeField() {
+ $field_name = $this->fieldStorage->getName();
+
+ // Ensure field is set to a all-day field.
+ $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_TYPE_ALLDAY);
+ $this->fieldStorage->save();
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+ $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.');
+ $this->assertFieldByName("{$field_name}[0][value2][date]", '', 'End date element found.');
+ $this->assertFieldByXPath('//*[@id="edit-' . $field_name . '-wrapper"]/h4[contains(@class, "js-form-required")]', TRUE, 'Required markup found');
+ $this->assertNoFieldByName("{$field_name}[0][value][time]", '', 'Start time element not found.');
+ $this->assertNoFieldByName("{$field_name}[0][value2][time]", '', 'End time element not found.');
+
+ // Build up dates in the proper timezone.
+ $value = '2012-12-31 00:00:00';
+ $start_date = new DrupalDateTime($value, timezone_open(drupal_get_user_timezone()));
+ $value2 = '2013-06-06 23:59:59';
+ $end_date = new DrupalDateTime($value2, timezone_open(drupal_get_user_timezone()));
+
+ // Submit a valid date and ensure it is accepted.
+ $date_format = DateFormat::load('html_date')->getPattern();
+ $time_format = DateFormat::load('html_time')->getPattern();
+
+ $edit = array(
+ "{$field_name}[0][value][date]" => $start_date->format($date_format),
+ "{$field_name}[0][value2][date]" => $end_date->format($date_format),
+ );
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+ $id = $match[1];
+ $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
+ $this->assertRaw($start_date->format($date_format));
+ $this->assertNoRaw($start_date->format($time_format));
+ $this->assertRaw($end_date->format($date_format));
+ $this->assertNoRaw($end_date->format($time_format));
+
+ // Verify that the default formatter works.
+ $this->displayOptions['settings'] = [
+ 'format_type' => 'long',
+ 'separator' => 'THESEPARATOR',
+ ] + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+
+ $start_expected = format_date($start_date->getTimestamp(), 'long');
+ $start_expected_iso = format_date($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+ $end_expected = format_date($end_date->getTimestamp(), 'long');
+ $end_expected_iso = format_date($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+ $this->renderTestEntity($id);
+ $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, SafeMarkup::format('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $start_expected, '%expected_iso' => $start_expected_iso]));
+ $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, SafeMarkup::format('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $end_expected, '%expected_iso' => $end_expected_iso]));
+ $this->assertText(' THESEPARATOR ', 'Found proper separator');
+
+ // Verify that the plain formatter works.
+ $this->displayOptions['type'] = 'daterange_plain';
+ $this->displayOptions['settings'] = $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format(DATETIME_DATETIME_STORAGE_FORMAT) . ' - ' . $end_date->format(DATETIME_DATETIME_STORAGE_FORMAT);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, SafeMarkup::format('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected)));
+
+ // Verify that the custom formatter works.
+ $this->displayOptions['type'] = 'daterange_custom';
+ $this->displayOptions['settings'] = array('date_format' => 'm/d/Y') + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' - ' . $end_date->format($this->displayOptions['settings']['date_format']);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, SafeMarkup::format('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+
+ // Verify that the 'timezone_override' setting works.
+ $this->displayOptions['type'] = 'daterange_custom';
+ $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A', 'timezone_override' => 'America/New_York'] + $this->defaultSettings;
+ entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+ ->setComponent($field_name, $this->displayOptions)
+ ->save();
+ $expected = $start_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
+ $expected .= ' - ' . $end_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
+ $this->renderTestEntity($id);
+ $this->assertText($expected, SafeMarkup::format('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+ }
+
+ /**
+ * Tests Date Range List Widget functionality.
+ */
+ function testDatelistWidget() {
+ $field_name = $this->fieldStorage->getName();
+
+ // Ensure field is set to a date only field.
+ $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_TYPE_DATE);
+ $this->fieldStorage->save();
+
+ // Change the widget to a datelist widget.
+ entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+ ->setComponent($field_name, [
+ 'type' => 'daterange_datelist',
+ 'settings' => [
+ 'date_order' => 'YMD',
+ ],
+ ])
+ ->save();
+ \Drupal::entityManager()->clearCachedFieldDefinitions();
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+
+ // Assert that Hour and Minute Elements do not appear on Date Only
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element not found on Date Only.');
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element not found on Date Only.');
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value2-hour\"]", NULL, 'Hour element not found on Date Only.');
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value2-minute\"]", NULL, 'Minute element not found on Date Only.');
+
+ // Go to the form display page to assert that increment option does not appear on Date Only
+ $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
+ $this->drupalGet($fieldEditUrl);
+
+ // Click on the widget settings button to open the widget settings form.
+ $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit");
+ $xpathIncr = "//select[starts-with(@id, \"edit-fields-$field_name-settings-edit-form-settings-increment\")]";
+ $this->assertNoFieldByXPath($xpathIncr, NULL, 'Increment element not found for Date Only.');
+
+ // Change the field is set to an all day field.
+ $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_TYPE_ALLDAY);
+ $this->fieldStorage->save();
+
+ // Change the widget to a datelist widget.
+ entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+ ->setComponent($field_name, [
+ 'type' => 'daterange_datelist',
+ 'settings' => [
+ 'date_order' => 'YMD',
+ ],
+ ])
+ ->save();
+ \Drupal::entityManager()->clearCachedFieldDefinitions();
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+
+ // Assert that Hour and Minute Elements do not appear on Date Only
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element not found on Date Only.');
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element not found on Date Only.');
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value2-hour\"]", NULL, 'Hour element not found on Date Only.');
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value2-minute\"]", NULL, 'Minute element not found on Date Only.');
+
+ // Go to the form display page to assert that increment option does not appear on Date Only
+ $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
+ $this->drupalGet($fieldEditUrl);
+
+ // Click on the widget settings button to open the widget settings form.
+ $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit");
+ $xpathIncr = "//select[starts-with(@id, \"edit-fields-$field_name-settings-edit-form-settings-increment\")]";
+ $this->assertNoFieldByXPath($xpathIncr, NULL, 'Increment element not found for Date Only.');
+
+ // Change the field to a datetime field.
+ $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_TYPE_DATETIME);
+ $this->fieldStorage->save();
+
+ // Change the widget to a datelist widget.
+ entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+ ->setComponent($field_name, [
+ 'type' => 'daterange_datelist',
+ 'settings' => [
+ 'increment' => 1,
+ 'date_order' => 'YMD',
+ 'time_type' => '12',
+ ],
+ ])
+ ->save();
+ \Drupal::entityManager()->clearCachedFieldDefinitions();
+
+ // Go to the form display page to assert that increment option does appear on Date Time
+ $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
+ $this->drupalGet($fieldEditUrl);
+
+ // Click on the widget settings button to open the widget settings form.
+ $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit");
+ $this->assertFieldByXPath($xpathIncr, NULL, 'Increment element found for Date and time.');
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-year\"]", NULL, 'Year element found.');
+ $this->assertOptionSelected("edit-$field_name-0-value-year", '', 'No year selected.');
+ $this->assertOptionByText("edit-$field_name-0-value-year", t('Year'));
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-month\"]", NULL, 'Month element found.');
+ $this->assertOptionSelected("edit-$field_name-0-value-month", '', 'No month selected.');
+ $this->assertOptionByText("edit-$field_name-0-value-month", t('Month'));
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-day\"]", NULL, 'Day element found.');
+ $this->assertOptionSelected("edit-$field_name-0-value-day", '', 'No day selected.');
+ $this->assertOptionByText("edit-$field_name-0-value-day", t('Day'));
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element found.');
+ $this->assertOptionSelected("edit-$field_name-0-value-hour", '', 'No hour selected.');
+ $this->assertOptionByText("edit-$field_name-0-value-hour", t('Hour'));
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element found.');
+ $this->assertOptionSelected("edit-$field_name-0-value-minute", '', 'No minute selected.');
+ $this->assertOptionByText("edit-$field_name-0-value-minute", t('Minute'));
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-second\"]", NULL, 'Second element not found.');
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-ampm\"]", NULL, 'AMPM element found.');
+ $this->assertOptionSelected("edit-$field_name-0-value-ampm", '', 'No ampm selected.');
+ $this->assertOptionByText("edit-$field_name-0-value-ampm", t('AM/PM'));
+
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value2-year\"]", NULL, 'Year element found.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-year", '', 'No year selected.');
+ $this->assertOptionByText("edit-$field_name-0-value2-year", t('Year'));
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value2-month\"]", NULL, 'Month element found.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-month", '', 'No month selected.');
+ $this->assertOptionByText("edit-$field_name-0-value2-month", t('Month'));
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value2-day\"]", NULL, 'Day element found.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-day", '', 'No day selected.');
+ $this->assertOptionByText("edit-$field_name-0-value2-day", t('Day'));
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value2-hour\"]", NULL, 'Hour element found.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-hour", '', 'No hour selected.');
+ $this->assertOptionByText("edit-$field_name-0-value2-hour", t('Hour'));
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value2-minute\"]", NULL, 'Minute element found.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-minute", '', 'No minute selected.');
+ $this->assertOptionByText("edit-$field_name-0-value2-minute", t('Minute'));
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value2-second\"]", NULL, 'Second element not found.');
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value2-ampm\"]", NULL, 'AMPM element found.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-ampm", '', 'No ampm selected.');
+ $this->assertOptionByText("edit-$field_name-0-value2-ampm", t('AM/PM'));
+
+ // Submit a valid date and ensure it is accepted.
+ $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 5, 'minute' => 15];
+ $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 30];
+
+ $edit = [];
+ // Add the ampm indicator since we are testing 12 hour time.
+ $start_date_value['ampm'] = 'am';
+ $end_date_value['ampm'] = 'pm';
+ foreach ($start_date_value as $part => $value) {
+ $edit["{$field_name}[0][value][$part]"] = $value;
+ }
+ foreach ($end_date_value as $part => $value) {
+ $edit["{$field_name}[0][value2][$part]"] = $value;
+ }
+
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+ $id = $match[1];
+ $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
+
+ $this->assertOptionSelected("edit-$field_name-0-value-year", '2012', 'Correct year selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-month", '12', 'Correct month selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-day", '31', 'Correct day selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-hour", '5', 'Correct hour selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-minute", '15', 'Correct minute selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-ampm", 'am', 'Correct ampm selected.');
+
+ $this->assertOptionSelected("edit-$field_name-0-value2-year", '2013', 'Correct year selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-month", '1', 'Correct month selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-day", '15', 'Correct day selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-hour", '3', 'Correct hour selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-minute", '30', 'Correct minute selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-ampm", 'pm', 'Correct ampm selected.');
+
+ // Test the widget using increment other than 1 and 24 hour mode.
+ entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+ ->setComponent($field_name, [
+ 'type' => 'daterange_datelist',
+ 'settings' => [
+ 'increment' => 15,
+ 'date_order' => 'YMD',
+ 'time_type' => '24',
+ ],
+ ])
+ ->save();
+ \Drupal::entityManager()->clearCachedFieldDefinitions();
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+
+ // Other elements are unaffected by the changed settings.
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element found.');
+ $this->assertOptionSelected("edit-$field_name-0-value-hour", '', 'No hour selected.');
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-ampm\"]", NULL, 'AMPM element not found.');
+ $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value2-hour\"]", NULL, 'Hour element found.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-hour", '', 'No hour selected.');
+ $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value2-ampm\"]", NULL, 'AMPM element not found.');
+
+ // Submit a valid date and ensure it is accepted.
+ $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 17, 'minute' => 15];
+ $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 30];
+
+ $edit = [];
+ foreach ($start_date_value as $part => $value) {
+ $edit["{$field_name}[0][value][$part]"] = $value;
+ }
+ foreach ($end_date_value as $part => $value) {
+ $edit["{$field_name}[0][value2][$part]"] = $value;
+ }
+
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+ $id = $match[1];
+ $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
+
+ $this->assertOptionSelected("edit-$field_name-0-value-year", '2012', 'Correct year selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-month", '12', 'Correct month selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-day", '31', 'Correct day selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-hour", '17', 'Correct hour selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value-minute", '15', 'Correct minute selected.');
+
+ $this->assertOptionSelected("edit-$field_name-0-value2-year", '2013', 'Correct year selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-month", '1', 'Correct month selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-day", '15', 'Correct day selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-hour", '3', 'Correct hour selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-minute", '30', 'Correct minute selected.');
+
+ // Test the widget for partial completion of fields.
+ entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+ ->setComponent($field_name, [
+ 'type' => 'daterange_datelist',
+ 'settings' => [
+ 'increment' => 1,
+ 'date_order' => 'YMD',
+ 'time_type' => '24',
+ ],
+ ])
+ ->save();
+ \Drupal::entityManager()->clearCachedFieldDefinitions();
+
+ // Test the widget for validation notifications.
+ foreach ($this->datelistDataProvider() as $data) {
+ list($start_date_value, $end_date_value, $expected) = $data;
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+
+ // Submit a partial date and ensure and error message is provided.
+ $edit = [];
+ foreach ($start_date_value as $part => $value) {
+ $edit["{$field_name}[0][value][$part]"] = $value;
+ }
+ foreach ($end_date_value as $part => $value) {
+ $edit["{$field_name}[0][value2][$part]"] = $value;
+ }
+
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertResponse(200);
+ foreach ($expected as $expected_text) {
+ $this->assertText(t($expected_text));
+ }
+ }
+
+ // Test the widget for complete input with zeros as part of selections.
+ $this->drupalGet('entity_test/add');
+
+ $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 0, 'minute' => 0];
+ $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 30];
+ $edit = [];
+ foreach ($start_date_value as $part => $value) {
+ $edit["{$field_name}[0][value][$part]"] = $value;
+ }
+ foreach ($end_date_value as $part => $value) {
+ $edit["{$field_name}[0][value2][$part]"] = $value;
+ }
+
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertResponse(200);
+ preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+ $id = $match[1];
+ $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
+
+ // Test the widget to ensure zeros are not deselected on validation.
+ $this->drupalGet('entity_test/add');
+
+ $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 0, 'minute' => 0];
+ $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 0];
+ $edit = [];
+ foreach ($start_date_value as $part => $value) {
+ $edit["{$field_name}[0][value][$part]"] = $value;
+ }
+ foreach ($end_date_value as $part => $value) {
+ $edit["{$field_name}[0][value2][$part]"] = $value;
+ }
+
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertResponse(200);
+ $this->assertOptionSelected("edit-$field_name-0-value-minute", '0', 'Correct minute selected.');
+ $this->assertOptionSelected("edit-$field_name-0-value2-minute", '0', 'Correct minute selected.');
+ }
+
+ /**
+ * The data provider for testing the validation of the datelist widget.
+ *
+ * @return array
+ * An array of datelist input permutations to test.
+ */
+ protected function datelistDataProvider() {
+ return [
+ // Year only selected, validation error on Month, Day, Hour, Minute.
+ [['year' => 2012, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''],
+ ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [
+ 'A value must be selected for month.',
+ 'A value must be selected for day.',
+ 'A value must be selected for hour.',
+ 'A value must be selected for minute.',
+ ]],
+ // Year and Month selected, validation error on Day, Hour, Minute.
+ [['year' => 2012, 'month' => '12', 'day' => '', 'hour' => '', 'minute' => ''],
+ ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [
+ 'A value must be selected for day.',
+ 'A value must be selected for hour.',
+ 'A value must be selected for minute.',
+ ]],
+ // Year, Month and Day selected, validation error on Hour, Minute.
+ [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => ''],
+ ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [
+ 'A value must be selected for hour.',
+ 'A value must be selected for minute.',
+ ]],
+ // Year, Month, Day and Hour selected, validation error on Minute only.
+ [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => ''],
+ ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [
+ 'A value must be selected for minute.',
+ ]],
+ [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'],
+ ['year' => 2013, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''], [
+ 'A value must be selected for month.',
+ 'A value must be selected for day.',
+ 'A value must be selected for hour.',
+ 'A value must be selected for minute.',
+ ]],
+ // Year and Month selected, validation error on Day, Hour, Minute.
+ [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'],
+ ['year' => 2013, 'month' => '1', 'day' => '', 'hour' => '', 'minute' => ''], [
+ 'A value must be selected for day.',
+ 'A value must be selected for hour.',
+ 'A value must be selected for minute.',
+ ]],
+ // Year, Month and Day selected, validation error on Hour, Minute.
+ [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'],
+ ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '', 'minute' => ''], [
+ 'A value must be selected for hour.',
+ 'A value must be selected for minute.',
+ ]],
+ // Year, Month, Day and Hour selected, validation error on Minute only.
+ [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'],
+ ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => ''], [
+ 'A value must be selected for minute.',
+ ]],
+ ];
+ }
+
+ /**
+ * Test default value functionality.
+ */
+ function testDefaultValue() {
+ // Create a test content type.
+ $this->drupalCreateContentType(['type' => 'date_content']);
+
+ // Create a field storage with settings to validate.
+ $field_name = Unicode::strtolower($this->randomMachineName());
+ $field_storage = FieldStorageConfig::create([
+ 'field_name' => $field_name,
+ 'entity_type' => 'node',
+ 'type' => 'daterange',
+ 'settings' => ['daterange_type' => DateRangeItem::DATERANGE_TYPE_DATE],
+ ]);
+ $field_storage->save();
+
+ $field = FieldConfig::create([
+ 'field_storage' => $field_storage,
+ 'bundle' => 'date_content',
+ ]);
+ $field->save();
+
+ // Set now as default_value.
+ $field_edit = [
+ 'default_value_input[default_start_date_type]' => 'now',
+ 'default_value_input[default_end_date_type]' => 'now',
+ ];
+ $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+
+ // Check that default value is selected in default value form.
+ $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
+ $this->assertOptionSelected('edit-default-value-input-default-start-date-type', 'now', 'The default start value is selected in instance settings page');
+ $this->assertFieldByName('default_value_input[default_start_date]', '', 'The relative start default value is empty in instance settings page');
+ $this->assertOptionSelected('edit-default-value-input-default-end-date-type', 'now', 'The default end value is selected in instance settings page');
+ $this->assertFieldByName('default_value_input[default_end_date]', '', 'The relative end default value is empty in instance settings page');
+
+ // Check if default_date has been stored successfully.
+ $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get();
+ $this->assertEqual($config_entity['default_value'][0], [
+ 'default_start_date_type' => 'now',
+ 'default_start_date' => 'now',
+ 'default_end_date_type' => 'now',
+ 'default_end_date' => 'now'
+ ], 'Default value has been stored successfully');
+
+ // Clear field cache in order to avoid stale cache values.
+ \Drupal::entityManager()->clearCachedFieldDefinitions();
+
+ // Create a new node to check that datetime field default value is today.
+ $new_node = Node::create(['type' => 'date_content']);
+ $expected_date = new DrupalDateTime('now', DATETIME_STORAGE_TIMEZONE);
+ $this->assertEqual($new_node->get($field_name)->offsetGet(0)->value, $expected_date->format(DATETIME_DATE_STORAGE_FORMAT));
+ $this->assertEqual($new_node->get($field_name)->offsetGet(0)->value2, $expected_date->format(DATETIME_DATE_STORAGE_FORMAT));
+
+ // Set an invalid relative default_value to test validation.
+ $field_edit = [
+ 'default_value_input[default_start_date_type]' => 'relative',
+ 'default_value_input[default_start_date]' => 'invalid date',
+ 'default_value_input[default_end_date_type]' => 'relative',
+ 'default_value_input[default_end_date]' => '+1 day',
+ ];
+ $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+ $this->assertText('The relative start date value entered is invalid.');
+
+ $field_edit = [
+ 'default_value_input[default_start_date_type]' => 'relative',
+ 'default_value_input[default_start_date]' => '+1 day',
+ 'default_value_input[default_end_date_type]' => 'relative',
+ 'default_value_input[default_end_date]' => 'invalid date',
+ ];
+ $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+ $this->assertText('The relative end date value entered is invalid.');
+
+ // Set a relative default_value.
+ $field_edit = [
+ 'default_value_input[default_start_date_type]' => 'relative',
+ 'default_value_input[default_start_date]' => '+45 days',
+ 'default_value_input[default_end_date_type]' => 'relative',
+ 'default_value_input[default_end_date]' => '+90 days',
+ ];
+ $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+
+ // Check that default value is selected in default value form.
+ $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
+ $this->assertOptionSelected('edit-default-value-input-default-start-date-type', 'relative', 'The default start value is selected in instance settings page');
+ $this->assertFieldByName('default_value_input[default_start_date]', '+45 days', 'The relative default start value is displayed in instance settings page');
+ $this->assertOptionSelected('edit-default-value-input-default-end-date-type', 'relative', 'The default end value is selected in instance settings page');
+ $this->assertFieldByName('default_value_input[default_end_date]', '+90 days', 'The relative default end value is displayed in instance settings page');
+
+ // Check if default_date has been stored successfully.
+ $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get();
+ $this->assertEqual($config_entity['default_value'][0], [
+ 'default_start_date_type' => 'relative',
+ 'default_start_date' => '+45 days',
+ 'default_end_date_type' => 'relative',
+ 'default_end_date' => '+90 days',
+ ], 'Default value has been stored successfully');
+
+ // Clear field cache in order to avoid stale cache values.
+ \Drupal::entityManager()->clearCachedFieldDefinitions();
+
+ // Create a new node to check that datetime field default value is +90 days.
+ $new_node = Node::create(['type' => 'date_content']);
+ $expected_start_date = new DrupalDateTime('+45 days', DATETIME_STORAGE_TIMEZONE);
+ $expected_end_date = new DrupalDateTime('+90 days', DATETIME_STORAGE_TIMEZONE);
+ $this->assertEqual($new_node->get($field_name)->offsetGet(0)->value, $expected_start_date->format(DATETIME_DATE_STORAGE_FORMAT));
+ $this->assertEqual($new_node->get($field_name)->offsetGet(0)->value2, $expected_end_date->format(DATETIME_DATE_STORAGE_FORMAT));
+
+ // Remove default value.
+ $field_edit = [
+ 'default_value_input[default_start_date_type]' => '',
+ 'default_value_input[default_end_date_type]' => '',
+ ];
+ $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+
+ // Check that default value is selected in default value form.
+ $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
+ $this->assertOptionSelected('edit-default-value-input-default-start-date-type', '', 'The default start value is selected in instance settings page');
+ $this->assertFieldByName('default_value_input[default_start_date]', '', 'The relative default start value is empty in instance settings page');
+ $this->assertOptionSelected('edit-default-value-input-default-end-date-type', '', 'The default end value is selected in instance settings page');
+ $this->assertFieldByName('default_value_input[default_end_date]', '', 'The relative default end value is empty in instance settings page');
+
+ // Check if default_date has been stored successfully.
+ $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get();
+ $this->assertTrue(empty($config_entity['default_value']), 'Empty default value has been stored successfully');
+
+ // Clear field cache in order to avoid stale cache values.
+ \Drupal::entityManager()->clearCachedFieldDefinitions();
+
+ // Create a new node to check that datetime field default value is not set.
+ $new_node = Node::create(['type' => 'date_content']);
+ $this->assertNull($new_node->get($field_name)->value, 'Default value is not set');
+ }
+
+ /**
+ * Test that invalid values are caught and marked as invalid.
+ */
+ function testInvalidField() {
+ // Change the field to a datetime field.
+ $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_TYPE_DATETIME);
+ $this->fieldStorage->save();
+ $field_name = $this->fieldStorage->getName();
+
+ // Display creation form.
+ $this->drupalGet('entity_test/add');
+ $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.');
+ $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Start time element found.');
+ $this->assertFieldByName("{$field_name}[0][value2][date]", '', 'End date element found.');
+ $this->assertFieldByName("{$field_name}[0][value2][time]", '', 'End time element found.');
+
+ // Submit invalid start dates and ensure they is not accepted.
+ $date_value = '';
+ $edit = [
+ "{$field_name}[0][value][date]" => $date_value,
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][value2][date]" => '2012-12-01',
+ "{$field_name}[0][value2][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', 'Empty start date value has been caught.');
+
+ $date_value = 'aaaa-12-01';
+ $edit = [
+ "{$field_name}[0][value][date]" => $date_value,
+ "{$field_name}[0][value][time]" => '00:00:00',
+ "{$field_name}[0][value2][date]" => '2012-12-01',
+ "{$field_name}[0][value2][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', format_string('Invalid start year value %date has been caught.', ['%date' => $date_value]));
+
+ $date_value = '2012-75-01';
+ $edit = [
+ "{$field_name}[0][value][date]" => $date_value,
+ "{$field_name}[0][value][time]" => '00:00:00',
+ "{$field_name}[0][value2][date]" => '2012-12-01',
+ "{$field_name}[0][value2][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', format_string('Invalid start month value %date has been caught.', ['%date' => $date_value]));
+
+ $date_value = '2012-12-99';
+ $edit = [
+ "{$field_name}[0][value][date]" => $date_value,
+ "{$field_name}[0][value][time]" => '00:00:00',
+ "{$field_name}[0][value2][date]" => '2012-12-01',
+ "{$field_name}[0][value2][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', format_string('Invalid start day value %date has been caught.', ['%date' => $date_value]));
+
+ // Submit invalid start times and ensure they is not accepted.
+ $time_value = '';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => $time_value,
+ "{$field_name}[0][value2][date]" => '2012-12-01',
+ "{$field_name}[0][value2][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', 'Empty start time value has been caught.');
+
+ $time_value = '49:00:00';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => $time_value,
+ "{$field_name}[0][value2][date]" => '2012-12-01',
+ "{$field_name}[0][value2][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', format_string('Invalid start hour value %time has been caught.', ['%time' => $time_value]));
+
+ $time_value = '12:99:00';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => $time_value,
+ "{$field_name}[0][value2][date]" => '2012-12-01',
+ "{$field_name}[0][value2][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', format_string('Invalid start minute value %time has been caught.', ['%time' => $time_value]));
+
+ $time_value = '12:15:99';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => $time_value,
+ "{$field_name}[0][value2][date]" => '2012-12-01',
+ "{$field_name}[0][value2][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', format_string('Invalid start second value %time has been caught.', ['%time' => $time_value]));
+
+ // Submit invalid end dates and ensure they is not accepted.
+ $date_value = '';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][value2][date]" => $date_value,
+ "{$field_name}[0][value2][time]" => '12:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', 'Empty end date value has been caught.');
+
+ $date_value = 'aaaa-12-01';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][value2][date]" => $date_value,
+ "{$field_name}[0][value2][time]" => '00:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', format_string('Invalid end year value %date has been caught.', ['%date' => $date_value]));
+
+ $date_value = '2012-75-01';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][value2][date]" => $date_value,
+ "{$field_name}[0][value2][time]" => '00:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', format_string('Invalid end month value %date has been caught.', ['%date' => $date_value]));
+
+ $date_value = '2012-12-99';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][value2][date]" => $date_value,
+ "{$field_name}[0][value2][time]" => '00:00:00',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', format_string('Invalid end day value %date has been caught.', ['%date' => $date_value]));
+
+ // Submit invalid start times and ensure they is not accepted.
+ $time_value = '';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][value2][date]" => '2012-12-01',
+ "{$field_name}[0][value2][time]" => $time_value,
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', 'Empty end time value has been caught.');
+
+ $time_value = '49:00:00';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][value2][date]" => '2012-12-01',
+ "{$field_name}[0][value2][time]" => $time_value,
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', format_string('Invalid end hour value %time has been caught.', ['%time' => $time_value]));
+
+ $time_value = '12:99:00';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][value2][date]" => '2012-12-01',
+ "{$field_name}[0][value2][time]" => $time_value,
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', format_string('Invalid end minute value %time has been caught.', ['%time' => $time_value]));
+
+ $time_value = '12:15:99';
+ $edit = [
+ "{$field_name}[0][value][date]" => '2012-12-01',
+ "{$field_name}[0][value][time]" => '12:00:00',
+ "{$field_name}[0][value2][date]" => '2012-12-01',
+ "{$field_name}[0][value2][time]" => $time_value,
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText('date is invalid', format_string('Invalid end second value %time has been caught.', ['%time' => $time_value]));
+ }
+
+ /**
+ * Tests that 'Date' field storage setting form is disabled if field has data.
+ */
+ public function testDateStorageSettings() {
+ // Create a test content type.
+ $this->drupalCreateContentType(['type' => 'date_content']);
+
+ // Create a field storage with settings to validate.
+ $field_name = Unicode::strtolower($this->randomMachineName());
+ $field_storage = FieldStorageConfig::create([
+ 'field_name' => $field_name,
+ 'entity_type' => 'node',
+ 'type' => 'daterange',
+ 'settings' => [
+ 'daterange_type' => DateRangeItem::DATERANGE_TYPE_DATE,
+ ],
+ ]);
+ $field_storage->save();
+ $field = FieldConfig::create([
+ 'field_storage' => $field_storage,
+ 'field_name' => $field_name,
+ 'bundle' => 'date_content',
+ ]);
+ $field->save();
+
+ entity_get_form_display('node', 'date_content', 'default')
+ ->setComponent($field_name, [
+ 'type' => 'datetime_default',
+ ])
+ ->save();
+ $edit = [
+ 'title[0][value]' => $this->randomString(),
+ 'body[0][value]' => $this->randomString(),
+ $field_name . '[0][value][date]' => '2016-04-01',
+ $field_name . '[0][value2][date]' => '2016-04-02',
+ ];
+ $this->drupalPostForm('node/add/date_content', $edit, t('Save'));
+ $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name . '/storage');
+ $result = $this->xpath("//*[@id='edit-settings-daterange-type' and contains(@disabled, 'disabled')]");
+ $this->assertEqual(count($result), 1, "Changing daterange setting is disabled.");
+ $this->assertText('There is data for this field in the database. The field settings can no longer be changed.');
+ }
+
+
+ /**
+ * Renders a entity_test and sets the output in the internal browser.
+ *
+ * @param int $id
+ * The entity_test ID to render.
+ * @param string $view_mode
+ * (optional) The view mode to use for rendering. Defaults to 'full'.
+ * @param bool $reset
+ * (optional) Whether to reset the entity_test controller cache. Defaults to
+ * TRUE to simplify testing.
+ */
+ protected function renderTestEntity($id, $view_mode = 'full', $reset = TRUE) {
+ if ($reset) {
+ \Drupal::service('entity_type.manager')->getStorage('entity_test')->resetCache([$id]);
+ }
+ $entity = EntityTest::load($id);
+ $display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode);
+ $build = $display->build($entity);
+ $output = \Drupal::service('renderer')->renderRoot($build);
+ $this->setRawContent($output);
+ $this->verbose($output);
+ }
+
+}