3 namespace Drupal\datetime_range\Plugin\Field\FieldWidget;
5 use Drupal\Core\Datetime\DrupalDateTime;
6 use Drupal\Core\Field\FieldItemListInterface;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
9 use Drupal\datetime\Plugin\Field\FieldWidget\DateTimeWidgetBase;
10 use Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem;
13 * Base class for the 'daterange_*' widgets.
15 class DateRangeWidgetBase extends DateTimeWidgetBase {
20 public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
21 $element = parent::formElement($items, $delta, $element, $form, $form_state);
23 // Wrap all of the select elements with a fieldset.
24 $element['#theme_wrappers'][] = 'fieldset';
26 $element['#element_validate'][] = [$this, 'validateStartEnd'];
27 $element['value']['#title'] = $this->t('Start date');
29 $element['end_value'] = [
30 '#title' => $this->t('End date'),
31 ] + $element['value'];
33 if ($items[$delta]->start_date) {
34 /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
35 $start_date = $items[$delta]->start_date;
36 $element['value']['#default_value'] = $this->createDefaultValue($start_date, $element['value']['#date_timezone']);
39 if ($items[$delta]->end_date) {
40 /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
41 $end_date = $items[$delta]->end_date;
42 $element['end_value']['#default_value'] = $this->createDefaultValue($end_date, $element['end_value']['#date_timezone']);
51 public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
52 // The widget form element type has transformed the value to a
53 // DrupalDateTime object at this point. We need to convert it back to the
54 // storage timezone and format.
55 foreach ($values as &$item) {
56 if (!empty($item['value']) && $item['value'] instanceof DrupalDateTime) {
57 /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
58 $start_date = $item['value'];
59 switch ($this->getFieldSetting('datetime_type')) {
60 case DateRangeItem::DATETIME_TYPE_DATE:
61 // If this is a date-only field, set it to the default time so the
62 // timezone conversion can be reversed.
63 datetime_date_default_time($start_date);
64 $format = DATETIME_DATE_STORAGE_FORMAT;
67 case DateRangeItem::DATETIME_TYPE_ALLDAY:
68 // All day fields start at midnight on the starting date, but are
69 // stored like datetime fields, so we need to adjust the time.
70 // This function is called twice, so to prevent a double conversion
71 // we need to explicitly set the timezone.
72 $start_date->setTimeZone(timezone_open(drupal_get_user_timezone()));
73 $start_date->setTime(0, 0, 0);
74 $format = DATETIME_DATETIME_STORAGE_FORMAT;
78 $format = DATETIME_DATETIME_STORAGE_FORMAT;
81 // Adjust the date for storage.
82 $start_date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
83 $item['value'] = $start_date->format($format);
86 if (!empty($item['end_value']) && $item['end_value'] instanceof DrupalDateTime) {
87 /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
88 $end_date = $item['end_value'];
89 switch ($this->getFieldSetting('datetime_type')) {
90 case DateRangeItem::DATETIME_TYPE_DATE:
91 // If this is a date-only field, set it to the default time so the
92 // timezone conversion can be reversed.
93 datetime_date_default_time($end_date);
94 $format = DATETIME_DATE_STORAGE_FORMAT;
97 case DateRangeItem::DATETIME_TYPE_ALLDAY:
98 // All day fields end at midnight on the end date, but are
99 // stored like datetime fields, so we need to adjust the time.
100 // This function is called twice, so to prevent a double conversion
101 // we need to explicitly set the timezone.
102 $end_date->setTimeZone(timezone_open(drupal_get_user_timezone()));
103 $end_date->setTime(23, 59, 59);
104 $format = DATETIME_DATETIME_STORAGE_FORMAT;
108 $format = DATETIME_DATETIME_STORAGE_FORMAT;
111 // Adjust the date for storage.
112 $end_date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
113 $item['end_value'] = $end_date->format($format);
121 * #element_validate callback to ensure that the start date <= the end date.
123 * @param array $element
124 * An associative array containing the properties and children of the
125 * generic form element.
126 * @param \Drupal\Core\Form\FormStateInterface $form_state
127 * The current state of the form.
128 * @param array $complete_form
129 * The complete form structure.
131 public function validateStartEnd(array &$element, FormStateInterface $form_state, array &$complete_form) {
132 $start_date = $element['value']['#value']['object'];
133 $end_date = $element['end_value']['#value']['object'];
135 if ($start_date instanceof DrupalDateTime && $end_date instanceof DrupalDateTime) {
136 if ($start_date->getTimestamp() !== $end_date->getTimestamp()) {
137 $interval = $start_date->diff($end_date);
138 if ($interval->invert === 1) {
139 $form_state->setError($element, $this->t('The @title end date cannot be before the start date', ['@title' => $element['#title']]));
146 * Creates a date object for use as a default value.
148 * This will take a default value, apply the proper timezone for display in
149 * a widget, and set the default time for date-only fields.
151 * @param \Drupal\Core\Datetime\DrupalDateTime $date
152 * The UTC default date.
153 * @param string $timezone
154 * The timezone to apply.
156 * @return \Drupal\Core\Datetime\DrupalDateTime
157 * A date object for use as a default value in a field widget.
159 protected function createDefaultValue($date, $timezone) {
160 // The date was created and verified during field_load(), so it is safe to
161 // use without further inspection.
162 if ($this->getFieldSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE) {
163 // A date without time will pick up the current time, use the default
165 datetime_date_default_time($date);
167 $date->setTimezone(new \DateTimeZone($timezone));