diff --git a/config/install/datetime_extras.date_range_format.medium.yml b/config/install/datetime_extras.date_range_format.medium.yml new file mode 100644 index 0000000..073e0b2 --- /dev/null +++ b/config/install/datetime_extras.date_range_format.medium.yml @@ -0,0 +1,17 @@ +langcode: en +status: true +dependencies: { } +id: medium +label: Medium +date_settings: + default_pattern: 'j F Y' + separator: '–' + same_month_start_pattern: j + same_month_end_pattern: 'j F Y' + same_year_start_pattern: 'j F' + same_year_end_pattern: 'j F Y' +datetime_settings: + default_pattern: 'j F Y H:i' + separator: '–' + same_day_start_pattern: 'j F Y H:i' + same_day_end_pattern: 'H:i' diff --git a/config/schema/datetime_extras.schema.yml b/config/schema/datetime_extras.schema.yml new file mode 100644 index 0000000..d9dd2e5 --- /dev/null +++ b/config/schema/datetime_extras.schema.yml @@ -0,0 +1,58 @@ +datetime_extras.date_range_format.*: + type: config_entity + label: 'Date range format config' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + + date_settings: + type: mapping + label: 'Date settings' + mapping: + default_pattern: + type: string + label: 'Default pattern' + separator: + type: string + label: 'Separator' + same_month_start_pattern: + type: string + label: 'Same month start pattern' + same_month_end_pattern: + type: string + label: 'Same month end pattern' + same_year_start_pattern: + type: string + label: 'Same year start pattern' + same_year_end_pattern: + type: string + label: 'Same year end pattern' + + datetime_settings: + type: mapping + label: 'Datetime settings' + mapping: + default_pattern: + type: string + label: 'Default pattern' + separator: + type: string + label: 'Separator' + same_day_start_pattern: + type: string + label: 'Same day start pattern' + same_day_end_pattern: + type: string + label: 'Same day end pattern' + +field.formatter.settings.daterange_compact: + type: mapping + label: 'Date/time range compact display format settings' + mapping: + format_type: + type: string + label: 'Date/time range format' diff --git a/datetime_extras.links.action.yml b/datetime_extras.links.action.yml new file mode 100644 index 0000000..c4b2efa --- /dev/null +++ b/datetime_extras.links.action.yml @@ -0,0 +1,5 @@ +entity.date_range_format.add_form: + route_name: 'entity.date_range_format.add_form' + title: 'Add format' + appears_on: + - entity.date_range_format.collection diff --git a/datetime_extras.links.menu.yml b/datetime_extras.links.menu.yml new file mode 100644 index 0000000..c01d8e3 --- /dev/null +++ b/datetime_extras.links.menu.yml @@ -0,0 +1,6 @@ +entity.date_range_format.collection: + title: 'Date and time range formats' + route_name: entity.date_range_format.collection + description: 'Configure how date and time ranges are displayed.' + parent: system.admin_config_regional + weight: 0 diff --git a/datetime_extras.services.yml b/datetime_extras.services.yml new file mode 100644 index 0000000..321b60d --- /dev/null +++ b/datetime_extras.services.yml @@ -0,0 +1,4 @@ +services: + datetime_extras.date_range.formatter: + class: Drupal\datetime_extras\DateRangeFormatter + arguments: ['@entity_type.manager', '@date.formatter'] diff --git a/src/DateRangeFormatListBuilder.php b/src/DateRangeFormatListBuilder.php new file mode 100644 index 0000000..2fc2a14 --- /dev/null +++ b/src/DateRangeFormatListBuilder.php @@ -0,0 +1,151 @@ +dateRangeFormatter = $date_range_formatter; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('entity.manager')->getStorage($entity_type->id()), + $container->get('datetime_extras.date_range.formatter') + ); + } + + /** + * {@inheritdoc} + */ + public function buildHeader() { + $header['label'] = $this->t('Name'); + $header['date'] = $this->t('Date examples'); + $header['datetime'] = $this->t('Date & time examples'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + /** @var \Drupal\datetime_extras\Entity\DateRangeFormatInterface $format */ + $format = $entity; + + $row['label'] = $format->label(); + $row['date']['data'] = $this->dateExamples($format); + $row['datetime']['data'] = $this->dateTimeExamples($format); + return $row + parent::buildRow($entity); + } + + /** + * Examples of various date ranges shown using the given format. + * + * @param \Drupal\datetime_extras\Entity\DateRangeFormatInterface $format + * The date range format entity. + * + * @return array + * A render array suitable for use within the list builder table. + */ + private function dateExamples(DateRangeFormatInterface $format) { + $examples = []; + + // An example range that is a single day. + $same_day_timestamp = \DateTime::createFromFormat('Y-m-d', '2017-01-01')->getTimestamp(); + $examples[] = $this->dateRangeFormatter->formatDateRange( + $same_day_timestamp, $same_day_timestamp, $format->id()); + + // An example range that spans several days within the same month. + $same_month_start_timestamp = \DateTime::createFromFormat('Y-m-d', '2017-01-02')->getTimestamp(); + $same_month_end_timestamp = \DateTime::createFromFormat('Y-m-d', '2017-01-03')->getTimestamp(); + $examples[] = $this->dateRangeFormatter->formatDateRange( + $same_month_start_timestamp, $same_month_end_timestamp, $format->id()); + + // An example range that spans several months within the same year. + $same_year_start_timestamp = \DateTime::createFromFormat('Y-m-d', '2017-01-04')->getTimestamp(); + $same_year_end_timestamp = \DateTime::createFromFormat('Y-m-d', '2017-02-05')->getTimestamp(); + $examples[] = $this->dateRangeFormatter->formatDateRange( + $same_year_start_timestamp, $same_year_end_timestamp, $format->id()); + + // An example range that spans multiple years. + $fallback_start_timestamp = \DateTime::createFromFormat('Y-m-d', '2017-01-06')->getTimestamp(); + $fallback_end_timestamp = \DateTime::createFromFormat('Y-m-d', '2018-01-07')->getTimestamp(); + $examples[] = $this->dateRangeFormatter->formatDateRange( + $fallback_start_timestamp, $fallback_end_timestamp, $format->id()); + + $output = ''; + foreach ($examples as $example) { + $output .= htmlspecialchars($example) . '
'; + } + return ['#markup' => $output]; + } + + /** + * Examples of various datetime ranges shown using the given format. + * + * @param \Drupal\datetime_extras\Entity\DateRangeFormatInterface $format + * The date range format entity. + * + * @return array + * A render array suitable for use within the list builder table. + */ + private function dateTimeExamples(DateRangeFormatInterface $format) { + $examples = []; + + // An example range that is a single date and time. + $same_time_timestamp = \DateTime::createFromFormat('Y-m-d H:i', '2017-01-01 09:00')->getTimestamp(); + $examples[] = $this->dateRangeFormatter->formatDateTimeRange( + $same_time_timestamp, $same_time_timestamp, $format->id()); + + // An example range that is contained within a single day. + $same_day_start_timestamp = \DateTime::createFromFormat('Y-m-d H:i', '2017-01-01 09:00')->getTimestamp(); + $same_day_end_timestamp = \DateTime::createFromFormat('Y-m-d H:i', '2017-01-01 13:00')->getTimestamp(); + $examples[] = $this->dateRangeFormatter->formatDateTimeRange( + $same_day_start_timestamp, $same_day_end_timestamp, $format->id()); + + // An example range that spans multiple days. + $fallback_start_timestamp = \DateTime::createFromFormat('Y-m-d H:i', '2017-01-01 09:00')->getTimestamp(); + $fallback_end_timestamp = \DateTime::createFromFormat('Y-m-d H:i', '2017-01-02 13:00')->getTimestamp(); + $examples[] = $this->dateRangeFormatter->formatDateTimeRange( + $fallback_start_timestamp, $fallback_end_timestamp, $format->id()); + + $output = ''; + foreach ($examples as $example) { + $output .= htmlspecialchars($example) . '
'; + } + return ['#markup' => $output]; + } + +} diff --git a/src/DateRangeFormatter.php b/src/DateRangeFormatter.php new file mode 100644 index 0000000..b71fb49 --- /dev/null +++ b/src/DateRangeFormatter.php @@ -0,0 +1,135 @@ +dateRangeFormatStorage = $entity_type_manager->getStorage('date_range_format'); + $this->dateFormatter = $date_formatter; + } + + /** + * {@inheritdoc} + */ + public function formatDateRange($start_timestamp, $end_timestamp, $type = 'medium', $timezone = NULL, $langcode = NULL) { + $start_date_time = DrupalDateTime::createFromTimestamp($start_timestamp, $timezone); + $end_date_time = DrupalDateTime::createFromTimestamp($end_timestamp, $timezone); + + /** @var \Drupal\datetime_extras\Entity\DateRangeFormatInterface $entity */ + $entity = $this->dateRangeFormatStorage->load($type); + $date_settings = $entity->getDateSettings(); + $default_pattern = $date_settings['default_pattern']; + $separator = $date_settings['separator'] ?: ''; + + // Strings containing the ISO-8601 representations of the start and end + // date can be used to determine if the day, month or year is the same. + $start_iso_8601 = $start_date_time->format('Y-m-d'); + $end_iso_8601 = $end_date_time->format('Y-m-d'); + + if ($start_iso_8601 === $end_iso_8601) { + // The range is a single day. + return $this->dateFormatter->format($start_timestamp, 'custom', + $default_pattern, $timezone, $langcode); + } + elseif (substr($start_iso_8601, 0, 7) === substr($end_iso_8601, 0, 7)) { + // The range spans several days within the same month. + $start_pattern = isset($date_settings['same_month_start_pattern']) ? $date_settings['same_month_start_pattern'] : ''; + $end_pattern = isset($date_settings['same_month_end_pattern']) ? $date_settings['same_month_end_pattern'] : ''; + if ($start_pattern && $end_pattern) { + $start_text = $this->dateFormatter->format($start_timestamp, 'custom', $start_pattern, $timezone, $langcode); + $end_text = $this->dateFormatter->format($end_timestamp, 'custom', $end_pattern, $timezone, $langcode); + return $start_text . $separator . $end_text; + } + } + elseif (substr($start_iso_8601, 0, 4) === substr($end_iso_8601, 0, 4)) { + // The range spans several months within the same year. + $start_pattern = isset($date_settings['same_year_start_pattern']) ? $date_settings['same_year_start_pattern'] : ''; + $end_pattern = isset($date_settings['same_year_end_pattern']) ? $date_settings['same_year_end_pattern'] : ''; + if ($start_pattern && $end_pattern) { + $start_text = $this->dateFormatter->format($start_timestamp, 'custom', $start_pattern, $timezone, $langcode); + $end_text = $this->dateFormatter->format($end_timestamp, 'custom', $end_pattern, $timezone, $langcode); + return $start_text . $separator . $end_text; + } + } + + // Fallback: show the start and end dates in full using the default + // pattern. This is the case if the range spans different years, + // or if the other patterns are not specified. + $start_text = $this->dateFormatter->format($start_timestamp, 'custom', $default_pattern, $timezone, $langcode); + $end_text = $this->dateFormatter->format($end_timestamp, 'custom', $default_pattern, $timezone, $langcode); + return $start_text . $separator . $end_text; + } + + /** + * {@inheritdoc} + */ + public function formatDateTimeRange($start_timestamp, $end_timestamp, $type = 'medium', $timezone = NULL, $langcode = NULL) { + $start_date_time = DrupalDateTime::createFromTimestamp($start_timestamp, $timezone); + $end_date_time = DrupalDateTime::createFromTimestamp($end_timestamp, $timezone); + + /** @var \Drupal\datetime_extras\Entity\DateRangeFormatInterface $entity */ + $entity = $this->dateRangeFormatStorage->load($type); + $datetime_settings = $entity->getDateTimeSettings(); + $default_pattern = $datetime_settings['default_pattern']; + $separator = $datetime_settings['separator'] ?: ''; + + // Strings containing the ISO-8601 representations of the start and end + // datetime can be used to determine if the date and/or time are the same. + $start_iso_8601 = $start_date_time->format('Y-m-d\TH:i:s'); + $end_iso_8601 = $end_date_time->format('Y-m-d\TH:i:s'); + + if ($start_iso_8601 === $end_iso_8601) { + // The range is a single date and time. + return $this->dateFormatter->format($start_timestamp, 'custom', + $default_pattern, $timezone, $langcode); + } + elseif (substr($start_iso_8601, 0, 10) == substr($end_iso_8601, 0, 10)) { + // The range is contained within a single day. + $start_pattern = isset($datetime_settings['same_day_start_pattern']) ? $datetime_settings['same_day_start_pattern'] : ''; + $end_pattern = isset($datetime_settings['same_day_end_pattern']) ? $datetime_settings['same_day_end_pattern'] : ''; + if ($start_pattern && $end_pattern) { + $start_text = $this->dateFormatter->format($start_timestamp, 'custom', $start_pattern, $timezone, $langcode); + $end_text = $this->dateFormatter->format($end_timestamp, 'custom', $end_pattern, $timezone, $langcode); + return $start_text . $separator . $end_text; + } + } + + // Fallback: show the start and end datetimes in full using the default + // pattern. This is the case if the range spans different days, + // or if the other patterns are not specified. + $start_text = $this->dateFormatter->format($start_timestamp, 'custom', $default_pattern, $timezone, $langcode); + $end_text = $this->dateFormatter->format($end_timestamp, 'custom', $default_pattern, $timezone, $langcode); + return $start_text . $separator . $end_text; + } + +} diff --git a/src/DateRangeFormatterInterface.php b/src/DateRangeFormatterInterface.php new file mode 100644 index 0000000..e1711fa --- /dev/null +++ b/src/DateRangeFormatterInterface.php @@ -0,0 +1,66 @@ +get('date_settings'); + } + + /** + * {@inheritdoc} + */ + public function getDateTimeSettings() { + return $this->get('datetime_settings'); + } + + /** + * {@inheritdoc} + */ + public function getCacheTagsToInvalidate() { + return ['rendered']; + } + +} diff --git a/src/Entity/DateRangeFormatInterface.php b/src/Entity/DateRangeFormatInterface.php new file mode 100644 index 0000000..cf357d0 --- /dev/null +++ b/src/Entity/DateRangeFormatInterface.php @@ -0,0 +1,46 @@ +t('Are you sure you want to delete the date range format %name?', ['%name' => $this->entity->label()]); + } + +} diff --git a/src/Form/DateRangeFormatForm.php b/src/Form/DateRangeFormatForm.php new file mode 100644 index 0000000..19c0355 --- /dev/null +++ b/src/Form/DateRangeFormatForm.php @@ -0,0 +1,223 @@ +entity; + + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $format->label(), + '#description' => $this->t("Name of the date time range format."), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#default_value' => $format->id(), + '#machine_name' => [ + 'exists' => '\Drupal\datetime_extras\Entity\DateRangeFormat::load', + ], + '#disabled' => !$format->isNew(), + ]; + + $date_settings = $format->getDateSettings(); + + $form['date'] = [ + '#type' => 'vertical_tabs', + '#title' => $this->t('Date only formats'), + '#tree' => FALSE, + ]; + + $form['date']['basic'] = [ + '#type' => 'details', + '#title' => $this->t('Basic'), + '#open' => TRUE, + '#weight' => 1, + '#group' => 'date', + '#description' => $this->t('Basic date format used for single dates, or ranges that cannot be shown in a compact form.'), + ]; + + $form['date']['basic']['default_pattern'] = [ + '#type' => 'textfield', + '#title' => $this->t('Pattern'), + '#default_value' => $date_settings['default_pattern'] ?: '', + '#maxlength' => 100, + '#description' => $this->t('A user-defined date format. See the PHP manual for available options.'), + '#required' => TRUE, + '#parents' => ['date_settings', 'default_pattern'], + ]; + + $form['date']['basic']['separator'] = [ + '#type' => 'textfield', + '#title' => $this->t('Separator'), + '#default_value' => $date_settings['separator'] ?: '', + '#maxlength' => 100, + '#size' => 10, + '#description' => $this->t('Text between start and end dates.'), + '#required' => FALSE, + '#parents' => ['date_settings', 'separator'], + ]; + + $form['date']['same_month'] = [ + '#type' => 'details', + '#title' => $this->t('Same month'), + '#open' => TRUE, + '#weight' => 2, + '#group' => 'date', + '#description' => $this->t('Optional formatting of date ranges that span multiple days within the same month.'), + ]; + + $form['date']['same_month']['same_month_start_pattern'] = [ + '#type' => 'textfield', + '#title' => $this->t('Start date pattern'), + '#default_value' => $date_settings['same_month_start_pattern'] ?: '', + '#maxlength' => 100, + '#description' => $this->t('A user-defined date format. See the PHP manual for available options.'), + '#parents' => ['date_settings', 'same_month_start_pattern'], + ]; + + $form['date']['same_month']['same_month_end_pattern'] = [ + '#type' => 'textfield', + '#title' => $this->t('End date pattern'), + '#default_value' => $date_settings['same_month_end_pattern'] ?: '', + '#maxlength' => 100, + '#description' => $this->t('A user-defined date format. See the PHP manual for available options.'), + '#parents' => ['date_settings', 'same_month_end_pattern'], + ]; + + $form['date']['same_year'] = [ + '#type' => 'details', + '#title' => $this->t('Same year'), + '#open' => TRUE, + '#weight' => 2, + '#group' => 'date', + '#description' => $this->t('Optional formatting of date ranges that span multiple months within the same year.'), + ]; + + $form['date']['same_year']['same_year_start_pattern'] = [ + '#type' => 'textfield', + '#title' => $this->t('Start date pattern'), + '#default_value' => $date_settings['same_year_start_pattern'] ?: '', + '#maxlength' => 100, + '#description' => $this->t('A user-defined date format. See the PHP manual for available options.'), + '#parents' => ['date_settings', 'same_year_start_pattern'], + ]; + + $form['date']['same_year']['same_year_end_pattern'] = [ + '#type' => 'textfield', + '#title' => $this->t('End date pattern'), + '#default_value' => $date_settings['same_year_end_pattern'] ?: '', + '#maxlength' => 100, + '#description' => $this->t('A user-defined date format. See the PHP manual for available options.'), + '#parents' => ['date_settings', 'same_year_end_pattern'], + ]; + + $datetime_settings = $format->getDateTimeSettings(); + + $form['datetime'] = [ + '#type' => 'vertical_tabs', + '#title' => $this->t('Date & time formats'), + ]; + + $form['datetime']['basic'] = [ + '#type' => 'details', + '#title' => $this->t('Basic'), + '#open' => TRUE, + '#weight' => 1, + '#group' => 'datetime', + '#description' => $this->t('Basic date and time format used for single date/times, or ranges that cannot be shown in a compact form.'), + ]; + + $form['datetime']['basic']['default_pattern'] = [ + '#type' => 'textfield', + '#title' => $this->t('Pattern'), + '#default_value' => $datetime_settings['default_pattern'] ?: '', + '#maxlength' => 100, + '#description' => $this->t('A user-defined date format. See the PHP manual for available options.'), + '#required' => TRUE, + '#parents' => ['datetime_settings', 'default_pattern'], + ]; + + $form['datetime']['basic']['separator'] = [ + '#type' => 'textfield', + '#title' => $this->t('Separator'), + '#default_value' => $datetime_settings['separator'] ?: '', + '#maxlength' => 100, + '#size' => 10, + '#description' => $this->t('Text between start and end date/times.'), + '#required' => FALSE, + '#parents' => ['datetime_settings', 'separator'], + ]; + + $form['datetime']['same_day'] = [ + '#type' => 'details', + '#title' => $this->t('Same day'), + '#open' => TRUE, + '#weight' => 2, + '#group' => 'datetime', + '#description' => $this->t('Optional formatting of time ranges within a single day.'), + ]; + + $form['datetime']['same_day']['same_day_start_pattern'] = [ + '#type' => 'textfield', + '#title' => $this->t('Start date/time pattern'), + '#default_value' => $datetime_settings['same_day_start_pattern'] ?: '', + '#maxlength' => 100, + '#description' => $this->t('A user-defined date format. See the PHP manual for available options.'), + '#parents' => ['datetime_settings', 'same_day_start_pattern'], + ]; + + $form['datetime']['same_day']['same_day_end_pattern'] = [ + '#type' => 'textfield', + '#title' => $this->t('End date/time pattern'), + '#default_value' => $datetime_settings['same_day_end_pattern'] ?: '', + '#maxlength' => 100, + '#description' => $this->t('A user-defined date format. See the PHP manual for available options.'), + '#parents' => ['datetime_settings', 'same_day_end_pattern'], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $date_range_format = $this->entity; + $status = $date_range_format->save(); + + switch ($status) { + case SAVED_NEW: + drupal_set_message($this->t('Created the %label date range format.', [ + '%label' => $date_range_format->label(), + ])); + break; + + default: + drupal_set_message($this->t('Updated the %label date range format.', [ + '%label' => $date_range_format->label(), + ])); + } + $form_state->setRedirectUrl($date_range_format->toUrl('collection')); + } + +} diff --git a/src/Plugin/Field/FieldFormatter/DateRangeCompactFormatter.php b/src/Plugin/Field/FieldFormatter/DateRangeCompactFormatter.php new file mode 100644 index 0000000..d6d9f9e --- /dev/null +++ b/src/Plugin/Field/FieldFormatter/DateRangeCompactFormatter.php @@ -0,0 +1,168 @@ + elements, with + * configurable date formats (from the list of configured formats) and a + * separator. + * + * @FieldFormatter( + * id = "daterange_compact", + * label = @Translation("Compact"), + * field_types = { + * "daterange" + * } + * ) + */ +class DateRangeCompactFormatter extends FormatterBase implements ContainerFactoryPluginInterface { + + /** + * The date range formatter service. + * + * @var \Drupal\datetime_extras\DateRangeFormatterInterface + */ + protected $dateRangeFormatter; + + /** + * The date range format entity storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $dateRangeFormatStorage; + + /** + * Constructs a new DateRangeCompactFormatter. + * + * @param string $plugin_id + * The plugin_id for the formatter. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The definition of the field to which the formatter is associated. + * @param array $settings + * The formatter settings. + * @param string $label + * The formatter label display setting. + * @param string $view_mode + * The view mode. + * @param array $third_party_settings + * Third party settings. + * @param \Drupal\datetime_extras\DateRangeFormatterInterface $date_range_formatter + * The date formatter service. + * @param \Drupal\Core\Entity\EntityStorageInterface $date_range_format_storage + * The date format entity storage. + */ + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, DateRangeFormatterInterface $date_range_formatter, EntityStorageInterface $date_range_format_storage) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings); + + $this->dateRangeFormatter = $date_range_formatter; + $this->dateRangeFormatStorage = $date_range_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('datetime_extras.date_range.formatter'), + $container->get('entity_type.manager')->getStorage('date_range_format') + ); + } + + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return [ + 'format_type' => 'medium', + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $form = parent::settingsForm($form, $form_state); + + $format_types = $this->dateRangeFormatStorage->loadMultiple(); + $options = []; + foreach ($format_types as $type => $type_info) { + $options[$type] = $type_info->label(); + } + + $form['format_type'] = [ + '#type' => 'select', + '#title' => $this->t('Date and time range format'), + '#description' => $this->t("Choose a format for displaying the date and time range."), + '#options' => $options, + '#default_value' => $this->getSetting('format_type'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = parent::settingsSummary(); + $summary[] = $this->t('Format: @format', ['@format' => $this->getSetting('format_type')]); + return $summary; + } + + /** + * {@inheritdoc} + */ + public function viewElements(FieldItemListInterface $items, $langcode) { + $elements = []; + + foreach ($items as $delta => $item) { + if (!empty($item->start_date) && !empty($item->end_date)) { + $start_timestamp = $item->start_date->getTimestamp(); + $end_timestamp = $item->end_date->getTimestamp(); + $format = $this->getSetting('format_type'); + + if ($this->getFieldSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE) { + $timezone = DATETIME_STORAGE_TIMEZONE; + $text = $this->dateRangeFormatter->formatDateRange($start_timestamp, $end_timestamp, $format, $timezone); + } + else { + $timezone = drupal_get_user_timezone(); + $text = $this->dateRangeFormatter->formatDateTimeRange($start_timestamp, $end_timestamp, $format, $timezone); + } + + $elements[$delta] = [ + '#plain_text' => $text, + '#cache' => [ + 'contexts' => [ + 'timezone', + ], + ], + ]; + } + } + + return $elements; + } + +} diff --git a/src/Tests/DateRangeCompactFormatterTest.php b/src/Tests/DateRangeCompactFormatterTest.php new file mode 100644 index 0000000..c707449 --- /dev/null +++ b/src/Tests/DateRangeCompactFormatterTest.php @@ -0,0 +1,369 @@ +installConfig(['system']); + $this->installConfig(['field']); + $this->installConfig(['datetime_extras']); + $this->installEntitySchema('entity_test'); + + $this->entityType = 'entity_test'; + $this->bundle = $this->entityType; + $this->dateFieldName = Unicode::strtolower($this->randomMachineName()); + $this->dateTimeFieldName = Unicode::strtolower($this->randomMachineName()); + + // Create a typical format for USA. + $usa_format = DateRangeFormat::create([ + 'id' => 'usa', + 'label' => 'USA', + 'date_settings' => [ + 'default_pattern' => 'F jS, Y', + 'separator' => ' - ', + 'same_month_start_pattern' => 'F jS', + 'same_month_end_pattern' => 'jS, Y', + 'same_year_start_pattern' => 'F jS', + 'same_year_end_pattern' => 'F jS, Y', + ], + 'datetime_settings' => [ + 'default_pattern' => 'g:ia \o\n F jS, Y', + 'separator' => ' - ', + 'same_day_start_pattern' => 'g:ia', + 'same_day_end_pattern' => 'g:ia \o\n F jS, Y', + ], + ]); + $usa_format->save(); + + // Create a ISO-8601 format without any compact variations. + $iso_8601_format = DateRangeFormat::create([ + 'id' => 'iso_8601', + 'label' => 'ISO-8601', + 'date_settings' => [ + 'default_pattern' => 'Y-m-d', + 'separator' => ' - ', + ], + 'datetime_settings' => [ + 'default_pattern' => 'Y-m-d\TH:i:s', + 'separator' => ' - ', + ], + ]); + $iso_8601_format->save(); + + $date_field_storage = FieldStorageConfig::create([ + 'field_name' => $this->dateFieldName, + 'entity_type' => $this->entityType, + 'type' => 'daterange', + 'settings' => [ + 'datetime_type' => DateTimeItem::DATETIME_TYPE_DATE, + ], + ]); + $date_field_storage->save(); + + $date_field_instance = FieldConfig::create([ + 'field_storage' => $date_field_storage, + 'bundle' => $this->bundle, + 'label' => $this->randomMachineName(), + ]); + $date_field_instance->save(); + + $date_time_field_storage = FieldStorageConfig::create([ + 'field_name' => $this->dateTimeFieldName, + 'entity_type' => $this->entityType, + 'type' => 'daterange', + 'settings' => [ + 'datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME, + ], + ]); + $date_time_field_storage->save(); + + $date_time_field_instance = FieldConfig::create([ + 'field_storage' => $date_time_field_storage, + 'bundle' => $this->bundle, + 'label' => $this->randomMachineName(), + ]); + $date_time_field_instance->save(); + + $this->defaultDisplay = EntityViewDisplay::create([ + 'targetEntityType' => $this->entityType, + 'bundle' => $this->bundle, + 'mode' => 'default', + 'status' => TRUE, + ]); + $this->defaultDisplay->setComponent($this->dateFieldName, [ + 'type' => 'daterange_compact', + 'settings' => [ + 'format_type' => 'medium', + ], + ]); + $this->defaultDisplay->setComponent($this->dateTimeFieldName, [ + 'type' => 'daterange_compact', + 'settings' => [ + 'format_type' => 'medium', + ], + ]); + $this->defaultDisplay->save(); + } + + /** + * Renders fields of a given entity with a given display. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The entity object with attached fields to render. + * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display + * The display to render the fields in. + * + * @return string + * The rendered entity fields. + */ + protected function renderEntityFields(FieldableEntityInterface $entity, EntityViewDisplayInterface $display) { + $content = $display->build($entity); + $content = $this->render($content); + return $content; + } + + /** + * Tests the display of date-only range fields. + */ + public function testDateRanges() { + $all_data = []; + + // Same day. + $all_data[] = [ + 'start' => '2017-01-01', + 'end' => '2017-01-01', + 'expected' => [ + 'default' => '1 January 2017', + 'usa' => 'January 1st, 2017', + 'iso_8601' => '2017-01-01', + ], + ]; + + // Different days, same month. + $all_data[] = [ + 'start' => '2017-01-02', + 'end' => '2017-01-03', + 'expected' => [ + 'default' => '2–3 January 2017', + 'usa' => 'January 2nd - 3rd, 2017', + 'iso_8601' => '2017-01-02 - 2017-01-03', + ], + ]; + + // Different months, same year. + $all_data[] = [ + 'start' => '2017-01-04', + 'end' => '2017-02-05', + 'expected' => [ + 'default' => '4 January–5 February 2017', + 'usa' => 'January 4th - February 5th, 2017', + 'iso_8601' => '2017-01-04 - 2017-02-05', + ], + ]; + + // Different years. + $all_data[] = [ + 'start' => '2017-01-06', + 'end' => '2018-02-07', + 'expected' => [ + 'default' => '6 January 2017–7 February 2018', + 'usa' => 'January 6th, 2017 - February 7th, 2018', + 'iso_8601' => '2017-01-06 - 2018-02-07', + ], + ]; + + foreach ($all_data as $data) { + foreach ($data['expected'] as $format => $expected) { + $entity = EntityTest::create([]); + $entity->{$this->dateFieldName}->value = $data['start']; + $entity->{$this->dateFieldName}->end_value = $data['end']; + + if ($format === 'default') { + $this->renderEntityFields($entity, $this->defaultDisplay); + } + else { + // For testing a custom format, don't use the entity's default + // display, but instead render the field using a temporary one. + $display = $this->buildCustomDisplay($this->dateFieldName, $format); + $this->renderEntityFields($entity, $display); + } + + $this->assertRaw($expected); + } + } + } + + /** + * Tests the display of date and time range fields. + */ + public function testDateTimeRanges() { + $all_data = []; + + // Note: the default timezone for unit tests is Australia/Sydney + // see https://www.drupal.org/node/2498619 for why + // Australia/Sydney is UTC +10:00 (normal) or UTC +11:00 (DST) + // DST starts first Sunday in October + // DST ends first Sunday in April. + // Same day. + $all_data[] = [ + 'start' => '2017-01-01T09:00:00', + 'end' => '2017-01-01T12:00:00', + 'expected' => [ + 'default' => '1 January 2017 20:00–23:00', + 'usa' => '8:00pm - 11:00pm on January 1st, 2017', + 'iso_8601' => '2017-01-01T20:00:00 - 2017-01-01T23:00:00', + ], + ]; + + // Different day in UTC, same day in Australia. + $all_data[] = [ + 'start' => '2017-01-01T23:00:00', + 'end' => '2017-01-02T01:00:00', + 'expected' => [ + 'default' => '2 January 2017 10:00–12:00', + 'usa' => '10:00am - 12:00pm on January 2nd, 2017', + 'iso_8601' => '2017-01-02T10:00:00 - 2017-01-02T12:00:00', + ], + ]; + + // Same day in UTC, different day in Australia. + $all_data[] = [ + 'start' => '2017-01-01T12:00:00', + 'end' => '2017-01-01T15:00:00', + 'expected' => [ + 'default' => '1 January 2017 23:00–2 January 2017 02:00', + 'usa' => '11:00pm on January 1st, 2017 - 2:00am on January 2nd, 2017', + 'iso_8601' => '2017-01-01T23:00:00 - 2017-01-02T02:00:00', + ], + ]; + + // Different days in UTC and Australia, also spans DST change. + $all_data[] = [ + 'start' => '2017-04-01T01:00:00', + 'end' => '2017-04-08T01:00:00', + 'expected' => [ + 'default' => '1 April 2017 12:00–8 April 2017 11:00', + 'usa' => '12:00pm on April 1st, 2017 - 11:00am on April 8th, 2017', + 'iso_8601' => '2017-04-01T12:00:00 - 2017-04-08T11:00:00', + ], + ]; + + foreach ($all_data as $data) { + foreach ($data['expected'] as $format => $expected) { + $entity = EntityTest::create([]); + $entity->{$this->dateTimeFieldName}->value = $data['start']; + $entity->{$this->dateTimeFieldName}->end_value = $data['end']; + + if ($format === 'default') { + $this->renderEntityFields($entity, $this->defaultDisplay); + } + else { + // For testing a custom format, don't use the entity's default + // display, but instead render the field using a temporary one. + $display = $this->buildCustomDisplay($this->dateTimeFieldName, $format); + $this->renderEntityFields($entity, $display); + } + + $this->assertRaw($expected); + } + } + } + + /** + * Helper function that creates a display object for the given format. + * + * @param string $field_name + * The name of the field. + * @param string $format + * The name of the format. + * + * @return \Drupal\Core\Entity\Display\EntityViewDisplayInterface + * A temporary display object. + */ + private function buildCustomDisplay($field_name, $format) { + $display = EntityViewDisplay::create([ + 'targetEntityType' => $this->entityType, + 'bundle' => $this->bundle, + 'mode' => 'default', + 'status' => TRUE, + ]); + $display->setComponent($field_name, [ + 'type' => 'daterange_compact', + 'settings' => [ + 'format_type' => $format, + ], + ]); + return $display; + } + +}