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.'); + } + +}