diff -u b/core/modules/image/css/editors/image.theme.css b/core/modules/image/css/editors/image.theme.css --- b/core/modules/image/css/editors/image.theme.css +++ b/core/modules/image/css/editors/image.theme.css @@ -70,11 +70,11 @@ padding: 5px; } -.quickedit-image-field-info label, .quickedit-image-field-info input { - margin-right: 5px; +.quickedit-image-field-info div { + margin-right: 10px; } -.quickedit-image-field-info input:last-child { +.quickedit-image-field-info div:last-child { margin-right: 0; } diff -u b/core/modules/image/js/editors/image.js b/core/modules/image/js/editors/image.js --- b/core/modules/image/js/editors/image.js +++ b/core/modules/image/js/editors/image.js @@ -8,8 +8,7 @@ 'use strict'; /** - * Theme function for validation errors of the Image module's in-place - * editor. + * Theme function for validation errors of the Image in-place editor. * * @param {object} settings * Settings object used to construct the markup. @@ -68,12 +67,16 @@ Drupal.theme.quickeditImageToolbar = _.template( '
' + '<% if (alt_field) { %>' + - ' ' + - ' required<%} %>/>' + + '
' + + ' ' + + ' required<%} %>/>' + + '
' + '<% } %>' + '<% if (title_field) { %>' + - ' ' + - ' required<%} %>/>' + + '
' + + ' ' + + ' required<%} %>/>' + + '
' + '<% } %>' + '
' ); @@ -152,7 +155,7 @@ // Render our initial dropzone element. Once the user reverts changes // or saves a new image, this element is removed. - var $dropzone = this.renderDropzone('upload', Drupal.t('Drag file here or click to upload')); + var $dropzone = this.renderDropzone('upload', Drupal.t('Drop file here or click to upload')); // Generic event callback to stop event behavior and bubbling. var stopEvent = function (e) { @@ -176,7 +179,7 @@ stopEvent(e); // Only respond when a file is dropped (could be another element). - if(e.originalEvent.dataTransfer && e.originalEvent.dataTransfer.files.length) { + if (e.originalEvent.dataTransfer && e.originalEvent.dataTransfer.files.length) { $(this).removeClass('hover'); self.uploadImage(e.originalEvent.dataTransfer.files[0]); } @@ -184,6 +187,9 @@ $dropzone.on('click', function (e) { stopEvent(e); + // Create an element without appending it to the DOM, and + // trigger a click event. This is the easiest way to arbitrarily + // open the browser's upload dialog. var $input = $('').trigger('click'); $input.change(function () { if (this.files.length) { @@ -223,7 +229,7 @@ */ uploadImage: function (file) { // Indicate loading by adding a special class to our icon. - this.renderDropzone('upload loading', Drupal.t('Uploading @file...', {'@file': file.name})); + this.renderDropzone('upload loading', Drupal.t('Uploading @file…', {'@file': file.name})); // Build a valid URL for our endpoint. var fieldID = this.fieldModel.get('fieldID'); @@ -243,10 +249,9 @@ self.fieldModel.get('entity').set('inTempStore', true); self.removeValidationErrors(); - // Replace our innerHTML with the new image. If we replaced - // our entire element with data.html, we would have to - // implement complicated logic like what's in - // Drupal.quickedit.AppView.renderUpdatedField. + // Replace our innerHTML with the new image. If we replaced our entire + // element with data.html, we would have to implement complicated logic + // like what's in Drupal.quickedit.AppView.renderUpdatedField. var content = $(response.html).closest('[data-quickedit-field-id]').html(); $el.html(content); }); @@ -259,9 +264,9 @@ * codes and messages by displaying them visually inline with the image. * * Drupal.ajax is not called here as the Form API is unused by this - * in-place editor, and our JSON requests/responses try to be editor - * agnostic. Ideally similar logic and routes could be used by modules - * like CKEditor for drag+drop file uploads as well. + * in-place editor, and our JSON requests/responses try to be + * editor-agnostic. Ideally similar logic and routes could be used by + * modules like CKEditor for drag+drop file uploads as well. * * @param {string} type * The type of request (i.e. GET, POST, PUT, DELETE, etc.) diff -u b/core/modules/image/src/Controller/QuickEditImageController.php b/core/modules/image/src/Controller/QuickEditImageController.php --- b/core/modules/image/src/Controller/QuickEditImageController.php +++ b/core/modules/image/src/Controller/QuickEditImageController.php @@ -68,8 +68,7 @@ } /** - * Returns a JSON object representing the new file upload, or validation - * errors. + * Returns JSON representing the new file upload, or validation errors. * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity of which an image field is being rendered. @@ -141,7 +140,7 @@ // Return a JSON object containing the errors from Drupal and our // "main_error", which is displayed inside the dropzone area. $messages = StatusMessages::renderMessages('error'); - return new JsonResponse(['errors' => $this->renderer->render($messages), 'main_error' => $this->t('The requested image failed validation.')]); + return new JsonResponse(['errors' => $this->renderer->render($messages), 'main_error' => $this->t('The image failed validation.')]); } } diff -u b/core/modules/image/tests/src/FunctionalJavascript/QuickEditImageTest.php b/core/modules/image/tests/src/FunctionalJavascript/QuickEditImageTest.php --- b/core/modules/image/tests/src/FunctionalJavascript/QuickEditImageTest.php +++ b/core/modules/image/tests/src/FunctionalJavascript/QuickEditImageTest.php @@ -23,17 +23,30 @@ public static $modules = ['node', 'image', 'field_ui', 'contextual', 'quickedit']; /** - * Tests if an image can be uploaded inline with Quick Edit. + * A user with permissions to administer content types and use Quick Edit. + * + * @var \Drupal\user\UserInterface */ - public function testUpload() { + protected $adminUser; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + // Create the Article node type. - if ($this->profile != 'standard') { - $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); - } + $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); - $admin = $this->drupalCreateUser(['access contextual links', 'access in-place editing', 'access content', 'access administration pages', 'administer site configuration', 'administer content types', 'administer node fields', 'administer nodes', 'create article content', 'edit any article content', 'delete any article content', 'administer node display']); - $this->drupalLogin($admin); + // Login as an admin user who can use Quick Edit and edit Articles. + $this->adminUser = $this->drupalCreateUser(['access contextual links', 'access in-place editing', 'access content', 'access administration pages', 'administer site configuration', 'administer content types', 'administer node fields', 'administer nodes', 'create article content', 'edit any article content', 'delete any article content', 'administer node display']); + $this->drupalLogin($this->adminUser); + } + /** + * Tests if an image can be uploaded inline with Quick Edit. + */ + public function testUpload() { // Create a field with a basic filetype restriction. $field_name = strtolower($this->randomMachineName()); $field_settings = [ @@ -61,7 +74,7 @@ // Create a File entity for the initial image. $file = File::create([ 'uri' => $valid_images[0]->uri, - 'uid' => $admin->id(), + 'uid' => $this->adminUser->id(), 'status' => FILE_STATUS_PERMANENT, ]); $file->save(); @@ -84,11 +97,14 @@ // Visit the new Node. $this->drupalGet('node/' . $node->id()); - // Assert that the initial image is present. - $this->assertSession()->elementExists('css', 'img[src*="' . $valid_images[0]->filename . '"]'); - + // Assemble common CSS selectors. $entity_selector = '[data-quickedit-entity-id="node/' . $node->id() . '"]'; $field_selector = '[data-quickedit-field-id="node/' . $node->id() . '/' . $field_name . '/' . $node->language()->getId() . '/full"]'; + $original_image_selector = 'img[src*="' . $valid_images[0]->filename . '"][alt="Hello world"]'; + $new_image_selector = 'img[src*="' . $valid_images[1]->filename . '"][alt="New text"]'; + + // Assert that the initial image is present. + $this->assertSession()->elementExists('css', $entity_selector . ' ' . $field_selector . ' ' . $original_image_selector); // Wait until Quick Edit loads. $condition = "jQuery('" . $entity_selector . " .quickedit').length > 0"; @@ -99,6 +115,12 @@ $this->click($entity_selector . ' [data-contextual-id] .quickedit > a'); $this->click($field_selector); + // Wait for the field info to load and set new alt text. + $condition = "jQuery('.quickedit-image-field-info').length > 0"; + $this->assertJsCondition($condition, 10000); + $input = $this->assertSession()->elementExists('css', '.quickedit-image-field-info input[name="alt"]'); + $input->setValue('New text'); + // Check that our Dropzone element exists. $this->assertSession()->elementExists('css', $field_selector . ' .quickedit-image-dropzone'); @@ -115,16 +137,27 @@ // Trigger the upload logic with a mock "drop" event. $script = 'var e = jQuery.Event("drop");' . 'e.originalEvent = {dataTransfer: {files: jQuery("#quickedit-image-test-input").get(0).files}};' - . 'e.preventDefault = e.stopPropagation = function() {};' + . 'e.preventDefault = e.stopPropagation = function () {};' . 'jQuery(".quickedit-image-dropzone").trigger(e);'; $this->getSession()->executeScript($script); - // Wait for AJAX to finish. + // Wait for the dropzone element to be removed (i.e. loading is done). + $condition = "jQuery('" . $field_selector . " .quickedit-image-dropzone').length == 0"; + $this->assertJsCondition($condition, 10000); + + // To prevent 403s on save, we re-set our request (cookie) state. + $this->prepareRequest(); + + // Save the change. + $this->triggerClick('.quickedit-button.action-save'); $this->assertSession()->assertWaitOnAjaxRequest(); + // Re-visit the page to make sure the edit worked. + $this->drupalGet('node/' . $node->id()); + // Check that the new image appears as expected. - $this->assertSession()->elementNotExists('css', 'img[src*="' . $valid_images[0]->filename . '"]'); - $this->assertSession()->elementExists('css', 'img[src*="' . $valid_images[1]->filename . '"]'); + $this->assertSession()->elementNotExists('css', $entity_selector . ' ' . $field_selector . ' ' . $original_image_selector); + $this->assertSession()->elementExists('css', $entity_selector . ' ' . $field_selector . ' ' . $new_image_selector); } /** diff -u b/core/themes/stable/css/image/editors/image.theme.css b/core/themes/stable/css/image/editors/image.theme.css --- b/core/themes/stable/css/image/editors/image.theme.css +++ b/core/themes/stable/css/image/editors/image.theme.css @@ -70,11 +70,11 @@ padding: 5px; } -.quickedit-image-field-info label, .quickedit-image-field-info input { - margin-right: 5px; +.quickedit-image-field-info div { + margin-right: 10px; } -.quickedit-image-field-info input:last-child { +.quickedit-image-field-info div:last-child { margin-right: 0; }