diff --git a/core/lib/Drupal/Core/Datetime/Element/Datelist.php b/core/lib/Drupal/Core/Datetime/Element/Datelist.php index 2f253b29e61..2caa4cad6b9 100644 --- a/core/lib/Drupal/Core/Datetime/Element/Datelist.php +++ b/core/lib/Drupal/Core/Datetime/Element/Datelist.php @@ -55,19 +55,21 @@ class Datelist extends DateElementBase { $date = NULL; if ($input !== FALSE) { $return = $input; - if (isset($input['ampm'])) { - if ($input['ampm'] == 'pm' && $input['hour'] < 12) { - $input['hour'] += 12; + if (empty(static::checkEmptyInputs($input, $parts))) { + if (isset($input['ampm'])) { + if ($input['ampm'] == 'pm' && $input['hour'] < 12) { + $input['hour'] += 12; + } + elseif ($input['ampm'] == 'am' && $input['hour'] == 12) { + $input['hour'] -= 12; + } + unset($input['ampm']); } - elseif ($input['ampm'] == 'am' && $input['hour'] == 12) { - $input['hour'] -= 12; + $timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL; + $date = DrupalDateTime::createFromArray($input, $timezone); + if ($date instanceOf DrupalDateTime && !$date->hasErrors()) { + static::incrementRound($date, $increment); } - unset($input['ampm']); - } - $timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL; - $date = DrupalDateTime::createFromArray($input, $timezone); - if ($date instanceOf DrupalDateTime && !$date->hasErrors()) { - static::incrementRound($date, $increment); } } else { @@ -250,7 +252,7 @@ class Datelist extends DateElementBase { $title = ''; } - $default = !empty($element['#value'][$part]) ? $element['#value'][$part] : ''; + $default = isset($element['#value'][$part]) && trim($element['#value'][$part]) != '' ? $element['#value'][$part] : ''; $value = $date instanceOf DrupalDateTime && !$date->hasErrors() ? $date->format($format) : $default; if (!empty($value) && $part != 'ampm') { $value = intval($value); @@ -265,7 +267,7 @@ class Datelist extends DateElementBase { '#attributes' => $element['#attributes'], '#options' => $options, '#required' => $element['#required'], - '#error_no_message' => TRUE, + '#error_no_message' => FALSE, ); } @@ -300,6 +302,7 @@ class Datelist extends DateElementBase { $input_exists = FALSE; $input = NestedArray::getValue($form_state->getValues(), $element['#parents'], $input_exists); if ($input_exists) { + $all_empty = static::checkEmptyInputs($input, $element['#date_part_order']); // If there's empty input and the field is not required, set it to empty. if (empty($input['year']) && empty($input['month']) && empty($input['day']) && !$element['#required']) { @@ -309,6 +312,11 @@ class Datelist extends DateElementBase { elseif (empty($input['year']) && empty($input['month']) && empty($input['day']) && $element['#required']) { $form_state->setError($element, t('The %field date is required.')); } + elseif (!empty($all_empty)) { + foreach ($all_empty as $value){ + $form_state->setError($element[$value], t('A value must be selected for %part.', array('%part' => $value))); + } + } else { // If the input is valid, set it. $date = $input['object']; @@ -317,12 +325,34 @@ class Datelist extends DateElementBase { } // If the input is invalid, set an error. else { - $form_state->setError($element, t('The %field date is invalid.')); + $form_state->setError($element, t('The %field date is invalid.', array('%field' => !empty($element['#title']) ? $element['#title'] : ''))); } } } } + /** + * Checks the input array for empty values. + * + * Input array keys are checked against values in the parts array. Elements + * not in the parts array are ignored. Returns an array representing elements + * from the input array that have no value. If no empty values are found, + * returned array is empty. + * + * @param array $input + * Array of individual inputs to check for value. + * @param array $parts + * Array to check input against, ignoring elements not in this array. + * + * @return array + * Array of keys from the input array that have no value, may be empty. + */ + protected static function checkEmptyInputs($input, $parts) { + // Filters out empty array values, any valid value would have a string length. + $filtered_input = array_filter($input, 'strlen'); + return array_diff($parts, array_keys($filtered_input)); + } + /** * Rounds minutes and seconds to nearest requested value. * diff --git a/core/modules/datetime/src/Tests/DateTimeFieldTest.php b/core/modules/datetime/src/Tests/DateTimeFieldTest.php index 50bd608d0c9..d5d14a4dc3f 100644 --- a/core/modules/datetime/src/Tests/DateTimeFieldTest.php +++ b/core/modules/datetime/src/Tests/DateTimeFieldTest.php @@ -519,6 +519,84 @@ class DateTimeFieldTest extends WebTestBase { $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.'); + + // Test the widget for partial completion of fields. + entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default') + ->setComponent($field_name, array( + 'type' => 'datetime_datelist', + 'settings' => array( + 'increment' => 1, + 'date_order' => 'YMD', + 'time_type' => '24', + ), + )) + ->save(); + \Drupal::entityManager()->clearCachedFieldDefinitions(); + + // Test the widget for validation notifications. + foreach ($this->datelistDataProvider() as $data) { + list($date_value, $expected) = $data; + + // Display creation form. + $this->drupalGet('entity_test/add'); + + // Submit a partial date and ensure and error message is provided. + $edit = array(); + foreach ($date_value as $part => $value) { + $edit["{$field_name}[0][value][$part]"] = $value; + } + + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertResponse(200); + $this->assertText(t($expected)); + } + + // Test the widget for complete input with zeros as part of selections. + $this->drupalGet('entity_test/add'); + + $date_value = array('year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'); + $edit = array(); + foreach ($date_value as $part => $value) { + $edit["{$field_name}[0][value][$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.', array('@id' => $id))); + + // Test the widget to ensure zeros are not deselected on validation. + $this->drupalGet('entity_test/add'); + + $date_value = array('year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => '0'); + $edit = array(); + foreach ($date_value as $part => $value) { + $edit["{$field_name}[0][value][$part]"] = $value; + } + + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertResponse(200); + $this->assertOptionSelected("edit-$field_name-0-value-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' => ''], '4 errors have been found: Month, Day, Hour, Minute'], + // Year and Month selected, validation error on Day, Hour, Minute. + [['year' => 2012, 'month' => '12', 'day' => '', 'hour' => '', 'minute' => ''], '3 errors have been found: Day, Hour, Minute'], + // Year, Month and Day selected, validation error on Hour, Minute. + [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => ''], '2 errors have been found: Hour, Minute'], + // Year, Month, Day and Hour selected, validation error on Minute only. + [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => ''], '1 error has been found: Minute'], + ]; } /**