diff --git a/core/lib/Drupal/Core/Form/FormErrorHandler.php b/core/lib/Drupal/Core/Form/FormErrorHandler.php index e2b3090..9a927ec 100644 --- a/core/lib/Drupal/Core/Form/FormErrorHandler.php +++ b/core/lib/Drupal/Core/Form/FormErrorHandler.php @@ -24,6 +24,14 @@ class FormErrorHandler implements FormErrorHandlerInterface { use LinkGeneratorTrait; /** + * Associated array of elements with errors which need to be linked to from + * the error message + * + * @var array[] + */ + protected $errorLinkElements = []; + + /** * Constructs a new FormErrorHandler. * * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation @@ -40,37 +48,84 @@ public function __construct(TranslationInterface $string_translation, LinkGenera * {@inheritdoc} */ public function handleFormErrors(array &$form, FormStateInterface $form_state) { - // Display error messages for each element. - $this->displayErrorMessages($form, $form_state); - // After validation, loop through and assign each element its errors. $this->setElementErrorsFromFormState($form, $form_state); + // Display error messages for each element. + $this->displayErrorMessages($form_state, $this->errorLinkElements); + + // Reset the list of elements with errors + $this->errorLinkElements = []; + return $this; } /** + * Stores the errors of each element directly on the element and keep a list + * of these elements + * + * We must provide a way for non-form functions to check the errors for a + * specific element. The most common usage of this is a #pre_render callback. + * + * @param array $elements + * An associative array containing the structure of a form element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + protected function setElementErrorsFromFormState(array &$elements, FormStateInterface &$form_state) { + // Recurse through all children. + foreach (Element::children($elements) as $key) { + if (isset($elements[$key]) && $elements[$key]) { + $this->setElementErrorsFromFormState($elements[$key], $form_state); + } + } + + // Store the errors for this element on the element directly and + // keep a list of elements with errors + // @todo get the element name differently when $elements['#name'] is not set + // like from #parents or the form structure + if (($elements['#errors'] = $form_state->getError($elements)) && !empty($elements['#name'])) { + $this->errorLinkElements[$elements['#name']] = $elements; + }; + } + + /** * Loops through and displays all form errors. * - * @param array $form - * An associative array containing the structure of the form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. + * @param array[] $error_elements + * An associative array of elements with errors */ - protected function displayErrorMessages(array $form, FormStateInterface $form_state) { + protected function displayErrorMessages(FormStateInterface $form_state, array $error_elements = []) { $error_links = []; - // Loop through all form errors and display a link for each error that is - // associated with a visible form element. - foreach ($form_state->getErrors() as $key => $error) { - if (($form_element = FormElementHelper::getElementByName($key, $form)) && Element::isVisibleElement($form_element)) { - $title = FormElementHelper::getElementTitle($form_element); - $error_links[] = $this->l($title, Url::fromRoute('', [], ['fragment' => 'edit-' . str_replace('_', '-', $key), 'external' => TRUE])); - } - else { - $this->drupalSetMessage($error, 'error'); + $errors = $form_state->getErrors(); + + // Create error links + foreach ($error_elements as $form_element) { + $title = FormElementHelper::getElementTitle($form_element); + + // Only show links to erroneous elements that are visible + $is_visible_element = Element::isVisibleElement($form_element) ? TRUE : FALSE; + // Don't show links to elements which use their parent element for inline errors + $not_use_parent = empty($form_element['#error_use_parent']); + // Only show links for elements that have a title themselves or have children with a title + $has_title = !empty($title); + + if ($is_visible_element && $not_use_parent && $has_title) { + $error_links[] = $this->l($title, Url::fromRoute('', [], [ + 'fragment' => $form_element['#id'], + 'external' => TRUE + ])); + unset($errors[implode('][', $form_element['#parents'])]); } } + // For all left over errors set normal error messages + foreach ($errors as $error) { + $this->drupalSetMessage($error, 'error'); + } + if (!empty($error_links)) { // We need to pass this through SafeMarkup::format() so // drupal_set_message() does not encode the links. @@ -88,28 +143,6 @@ protected function displayErrorMessages(array $form, FormStateInterface $form_st } /** - * Stores the errors of each element directly on the element. - * - * We must provide a way for non-form functions to check the errors for a - * specific element. The most common usage of this is a #pre_render callback. - * - * @param array $elements - * An associative array containing the structure of a form element. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - protected function setElementErrorsFromFormState(array &$elements, FormStateInterface &$form_state) { - // Recurse through all children. - foreach (Element::children($elements) as $key) { - if (isset($elements[$key]) && $elements[$key]) { - $this->setElementErrorsFromFormState($elements[$key], $form_state); - } - } - // Store the errors for this element on the element directly. - $elements['#errors'] = $form_state->getError($elements); - } - - /** * Wraps drupal_set_message(). * * @codeCoverageIgnore diff --git a/core/modules/content_translation/src/Tests/ContentTranslationUITest.php b/core/modules/content_translation/src/Tests/ContentTranslationUITest.php index 5a7429c..a6d50e1 100644 --- a/core/modules/content_translation/src/Tests/ContentTranslationUITest.php +++ b/core/modules/content_translation/src/Tests/ContentTranslationUITest.php @@ -241,7 +241,7 @@ protected function doTestAuthoringInfo() { 'content_translation[created]' => '19/11/1978', ); $this->drupalPostForm($entity->urlInfo('edit-form'), $edit, $this->getFormSubmitAction($entity, $langcode)); - $this->assertTrue($this->xpath('//div[contains(@class, "error")]//ul'), 'Invalid values generate a list of form errors.'); + $this->assertTrue($this->xpath('//div[contains(concat(" ", normalize-space(@class), " "), :class)]', array(':class' => ' messages--error ')), 'Invalid values generate a form error message.'); $metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode)); $this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly kept.'); $this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly kept.'); diff --git a/core/modules/file/src/Tests/FileFieldValidateTest.php b/core/modules/file/src/Tests/FileFieldValidateTest.php index 51d7402..ea8c0c9 100644 --- a/core/modules/file/src/Tests/FileFieldValidateTest.php +++ b/core/modules/file/src/Tests/FileFieldValidateTest.php @@ -34,7 +34,8 @@ function testRequired() { $edit = array(); $edit['title[0][value]'] = $this->randomMachineName(); $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish')); - $this->assertRaw(t('!title field is required.', array('!title' => $field->getLabel())), 'Node save failed when required file field was empty.'); + $this->assertText('1 error has been found: Choose a file', 'Node save failed when required file field was empty.'); + $this->assertIdentical(1, count($this->xpath('//div[contains(concat(" ", normalize-space(@class), " "), :class)]//a', array(':class' => ' messages--error '))), 'There is one link in the error message.'); // Create a new node with the uploaded file. $nid = $this->uploadNodeFile($test_file, $field_name, $type_name); @@ -55,7 +56,8 @@ function testRequired() { $edit = array(); $edit['title[0][value]'] = $this->randomMachineName(); $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish')); - $this->assertRaw(t('!title field is required.', array('!title' => $field->getLabel())), 'Node save failed when required multiple value file field was empty.'); + $this->assertText('1 error has been found: Choose a file', 'Node save failed when required multiple value file field was empty.'); + $this->assertIdentical(1, count($this->xpath('//div[contains(concat(" ", normalize-space(@class), " "), :class)]//a', array(':class' => ' messages--error '))), 'There is one link in the error message.'); // Create a new node with the uploaded file into the multivalue field. $nid = $this->uploadNodeFile($test_file, $field_name, $type_name); diff --git a/core/tests/Drupal/Tests/Core/Form/FormErrorHandlerTest.php b/core/tests/Drupal/Tests/Core/Form/FormErrorHandlerTest.php index 2769506..6bb11d5 100644 --- a/core/tests/Drupal/Tests/Core/Form/FormErrorHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Form/FormErrorHandlerTest.php @@ -44,11 +44,13 @@ public function testDisplayErrorMessages() { '#type' => 'textfield', '#title' => 'Test 1', '#parents' => ['test1'], + '#name' => 'test1', ]; $form['test2'] = [ '#type' => 'textfield', '#title' => 'Test 2', '#parents' => ['test2'], + '#name' => 'test2', ]; $form_state = new FormState(); $form_state->setErrorByName('test1', 'invalid'); @@ -75,6 +77,7 @@ public function testSetElementErrorsFromFormState() { '#type' => 'textfield', '#title' => 'Test', '#parents' => ['test'], + '#name' => 'test', ]; $form_state = new FormState(); $form_state->setErrorByName('test', 'invalid'); diff --git a/core/themes/seven/css/components/form.css b/core/themes/seven/css/components/form.css index 7db0a7f..bb9e1b4 100644 --- a/core/themes/seven/css/components/form.css +++ b/core/themes/seven/css/components/form.css @@ -43,7 +43,6 @@ label[for] { .form-disabled label { color: #737373; } - .form-disabled input.form-text, .form-disabled input.form-tel, .form-disabled input.form-email, @@ -58,7 +57,6 @@ label[for] { background-color: hsla(0, 0%, 0%, .08); box-shadow: none; } - .form-item input.error, .form-item textarea.error, .form-item select.error { @@ -68,12 +66,10 @@ label[for] { box-shadow: inset 0 5px 5px -5px #b8b8b8; color: #a51b00; } - .form-item textarea.error + .cke { border-width: 1px; border-color: #e62600; } - .form-item input.error:focus, .form-item textarea.error:focus, .form-item select.error:focus { @@ -88,16 +84,13 @@ label[for] { width: 7px; height: 7px; } - .form-error-message { margin-top: 0.15em; color: #ea2800; } - .fieldset-wrapper > .form-error-message { margin-top: 0; } - .text-format-wrapper .form-error-message { border: solid #ccc; border-width: 0 1px;