diff --git a/core/modules/image/css/editors/image.css b/core/modules/image/css/editors/image.css
new file mode 100644
index 0000000..08bb679
--- /dev/null
+++ b/core/modules/image/css/editors/image.css
@@ -0,0 +1,52 @@
+/**
+ * @file
+ * Functional styles for the Image module's in-place editor.
+ */
+
+/**
+ * A minimum width/height is required so that users can drag and drop files
+ * onto small images.
+ */
+.quickedit-image-element {
+ min-width: 200px;
+ min-height: 200px;
+}
+
+.quickedit-image-dropzone {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.quickedit-image-icon {
+ display: block;
+ width: 50px;
+ height: 50px;
+ background-repeat: no-repeat;
+ background-size: cover;
+}
+
+.quickedit-image-field-info {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+.quickedit-image-text {
+ display: block;
+}
+
+/**
+ * If we do not prevent pointer-events for child elements, our drag+drop events
+ * will not fire properly. This can lead to unintentional redirects if a file
+ * is dropped on a child element when a user intended to upload it.
+ */
+.quickedit-image-dropzone * {
+ pointer-events: none;
+}
diff --git a/core/modules/image/css/editors/image.theme.css b/core/modules/image/css/editors/image.theme.css
new file mode 100644
index 0000000..00bb093
--- /dev/null
+++ b/core/modules/image/css/editors/image.theme.css
@@ -0,0 +1,88 @@
+/**
+ * @file
+ * Theme styles for the Image module's in-place editor.
+ */
+
+.quickedit-image-dropzone {
+ background: rgba(116, 183, 255, 0.8);
+ transition: background .2s;
+}
+
+.quickedit-image-icon {
+ margin: 0 0 10px 0;
+ transition: margin .5s;
+}
+
+.quickedit-image-dropzone.hover {
+ background: rgba(116, 183, 255, 0.9);
+}
+
+.quickedit-image-dropzone.error {
+ background: rgba(255, 52, 27, 0.81);
+}
+
+.quickedit-image-dropzone.upload .quickedit-image-icon {
+ background-image: url('../../images/upload.svg');
+}
+
+.quickedit-image-dropzone.error .quickedit-image-icon {
+ background-image: url('../../images/error.svg');
+}
+
+.quickedit-image-dropzone.loading .quickedit-image-icon {
+ margin: -10px 0 20px 0;
+}
+
+.quickedit-image-dropzone.loading .quickedit-image-icon::after {
+ display: block;
+ content: "";
+ margin-left: -10px;
+ margin-top: -5px;
+ animation-duration: 2s;
+ animation-name: quickedit-image-spin;
+ animation-iteration-count: infinite;
+ animation-timing-function: linear;
+ width: 60px;
+ height: 60px;
+ border-style: solid;
+ border-radius: 35px;
+ border-width: 5px;
+ border-color: white transparent transparent transparent;
+}
+
+@keyframes quickedit-image-spin {
+ 0% {transform: rotate(0deg);}
+ 50% {transform: rotate(180deg);}
+ 100% {transform: rotate(360deg);}
+}
+
+.quickedit-image-text {
+ text-align: center;
+ color: white;
+ font-family: "Droid sans", "Lucida Grande", sans-serif;
+ font-size: 16px;
+ -webkit-user-select: none;
+}
+
+.quickedit-image-field-info {
+ background: rgba(0, 0, 0, 0.05);
+ border-top: 1px solid #c5c5c5;
+ padding: 5px;
+}
+
+.quickedit-image-field-info label, .quickedit-image-field-info input {
+ margin-right: 5px;
+}
+
+.quickedit-image-field-info input:last-child {
+ margin-right: 0;
+}
+
+.quickedit-image-errors .messages__wrapper {
+ margin: 0;
+ padding: 0;
+}
+
+.quickedit-image-errors .messages--error {
+ box-shadow: none;
+}
diff --git a/core/modules/image/image.install b/core/modules/image/image.install
index fd14d03..c044912 100644
--- a/core/modules/image/image.install
+++ b/core/modules/image/image.install
@@ -61,3 +61,10 @@ function image_requirements($phase) {
return $requirements;
}
+
+/**
+ * Flush caches as we changed field formatter metadata.
+ */
+function image_update_8201() {
+ // Empty update to trigger a cache flush.
+}
diff --git a/core/modules/image/image.libraries.yml b/core/modules/image/image.libraries.yml
index e9061a4..0ce1ba8 100644
--- a/core/modules/image/image.libraries.yml
+++ b/core/modules/image/image.libraries.yml
@@ -3,3 +3,15 @@ admin:
css:
theme:
css/image.admin.css: {}
+
+quickedit.inPlaceEditor.image:
+ version: VERSION
+ js:
+ js/editors/image.js: {}
+ css:
+ component:
+ css/editors/image.css: {}
+ theme:
+ css/editors/image.theme.css: {}
+ dependencies:
+ - quickedit/quickedit
diff --git a/core/modules/image/image.routing.yml b/core/modules/image/image.routing.yml
index ffeed86..4a0cb88 100644
--- a/core/modules/image/image.routing.yml
+++ b/core/modules/image/image.routing.yml
@@ -71,3 +71,29 @@ image.effect_edit_form:
route_callbacks:
- '\Drupal\image\Routing\ImageStyleRoutes::routes'
+
+image.upload:
+ path: '/quickedit/image/upload/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode_id}'
+ defaults:
+ _controller: '\Drupal\image\Controller\QuickEditImageController::upload'
+ options:
+ parameters:
+ entity:
+ type: entity:{entity_type}
+ requirements:
+ _permission: 'access in-place editing'
+ _access_quickedit_entity_field: 'TRUE'
+ _method: 'POST'
+
+image.info:
+ path: '/quickedit/image/info/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode_id}'
+ defaults:
+ _controller: '\Drupal\image\Controller\QuickEditImageController::getInfo'
+ options:
+ parameters:
+ entity:
+ type: entity:{entity_type}
+ requirements:
+ _permission: 'access in-place editing'
+ _access_quickedit_entity_field: 'TRUE'
+ _method: 'GET'
diff --git a/core/modules/image/images/error.svg b/core/modules/image/images/error.svg
new file mode 100644
index 0000000..1932ea4
--- /dev/null
+++ b/core/modules/image/images/error.svg
@@ -0,0 +1,4 @@
+
diff --git a/core/modules/image/images/upload.svg b/core/modules/image/images/upload.svg
new file mode 100644
index 0000000..168bc43
--- /dev/null
+++ b/core/modules/image/images/upload.svg
@@ -0,0 +1,4 @@
+
diff --git a/core/modules/image/js/editors/image.js b/core/modules/image/js/editors/image.js
new file mode 100644
index 0000000..342ac05
--- /dev/null
+++ b/core/modules/image/js/editors/image.js
@@ -0,0 +1,397 @@
+/**
+ * @file
+ * Drag+drop based in-place editor for images.
+ */
+
+(function ($, _, Drupal) {
+
+ 'use strict';
+
+ /**
+ * Theme function for validation errors of the Image module's in-place
+ * editor.
+ *
+ * @param {object} settings
+ * Settings object used to construct the markup.
+ * @param {string} settings.errors
+ * Already escaped HTML representing error messages.
+ *
+ * @return {string}
+ * The corresponding HTML.
+ */
+ Drupal.theme.quickeditImageErrors = _.template(
+ '
' +
+ ' <%= errors %>' +
+ '
'
+ );
+
+ /**
+ * Theme function for the dropzone element of the Image module's in-place
+ * editor.
+ *
+ * @param {object} settings
+ * Settings object used to construct the markup.
+ * @param {string} settings.text
+ * Text to display inline with the dropzone element.
+ *
+ * @return {string}
+ * The corresponding HTML.
+ */
+ Drupal.theme.quickeditImageDropzone = _.template(
+ '' +
+ ' ' +
+ ' <%- text %>' +
+ '
'
+ );
+
+ /**
+ * Theme function for the toolbar of the Image module's in-place editor.
+ *
+ * @param {object} settings
+ * Settings object used to construct the markup.
+ * @param {bool} settings.alt_field
+ * Whether or not the "Alt" field is enabled for this field.
+ * @param {bool} settings.alt_field_required
+ * Whether or not the "Alt" field is required for this field.
+ * @param {string} settings.alt
+ * The current value for the "Alt" field.
+ * @param {bool} settings.title_field
+ * Whether or not the "Title" field is enabled for this field.
+ * @param {bool} settings.title_field_required
+ * Whether or not the "Title" field is required for this field.
+ * @param {string} settings.title
+ * The current value for the "Title" field.
+ *
+ * @return {string}
+ * The corresponding HTML.
+ */
+ Drupal.theme.quickeditImageToolbar = _.template(
+ ''
+ );
+
+ Drupal.quickedit.editors.image = Drupal.quickedit.EditorView.extend(/** @lends Drupal.quickedit.editors.image# */{
+
+ /**
+ * @constructs
+ *
+ * @augments Drupal.quickedit.EditorView
+ *
+ * @param {object} options
+ * Options for the image editor.
+ */
+ initialize: function (options) {
+ Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
+ // Set our original value to our current HTML (for reverting).
+ this.model.set('originalValue', $.trim(this.$el.html()));
+ // $.val() callback function for copying input from our custom form to
+ // the Quick Edit Field Form.
+ this.model.set('currentValue', function (index, value) {
+ var matches = $(this).attr('name').match(/(alt|title)]$/);
+ if (matches) {
+ var name = matches[1];
+ var $toolgroup = $('#' + options.fieldModel.toolbarView.getMainWysiwygToolgroupId());
+ var $input = $toolgroup.find('.quickedit-image-field-info input[name="' + name + '"]');
+ if ($input.length) {
+ return $input.val();
+ }
+ }
+ });
+ },
+
+ /**
+ * @inheritdoc
+ *
+ * @param {Drupal.quickedit.FieldModel} fieldModel
+ * The field model that holds the state.
+ * @param {string} state
+ * The state to change to.
+ * @param {object} options
+ * State options, if needed by the state change.
+ */
+ stateChange: function (fieldModel, state, options) {
+ var from = fieldModel.previous('state');
+ switch (state) {
+ case 'inactive':
+ break;
+
+ case 'candidate':
+ if (from !== 'inactive') {
+ this.$el.find('.quickedit-image-dropzone').remove();
+ this.$el.removeClass('quickedit-image-element');
+ }
+ if (from === 'invalid') {
+ this.removeValidationErrors();
+ }
+ break;
+
+ case 'highlighted':
+ break;
+
+ case 'activating':
+ // Defer updating the field model until the current state change has
+ // propagated, to not trigger a nested state change event.
+ _.defer(function () {
+ fieldModel.set('state', 'active');
+ });
+ break;
+
+ case 'active':
+ var self = this;
+
+ // Indicate that this element is being edited by Quick Edit Image.
+ this.$el.addClass('quickedit-image-element');
+
+ // 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'));
+
+ // Generic event callback to stop event behavior and bubbling.
+ var stopEvent = function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ };
+
+ // Prevent the browser's default behavior when dragging files onto
+ // the document (usually opens them in the same tab).
+ $dropzone.on('dragover', stopEvent);
+ $dropzone.on('dragenter', function (e) {
+ stopEvent(e);
+ $(this).addClass('hover');
+ });
+ $dropzone.on('dragleave', function (e) {
+ stopEvent(e);
+ $(this).removeClass('hover');
+ });
+
+ $dropzone.on('drop', function (e) {
+ stopEvent(e);
+
+ // Only respond when a file is dropped (could be another element).
+ if(e.originalEvent.dataTransfer && e.originalEvent.dataTransfer.files.length) {
+ $(this).removeClass('hover');
+ self.uploadImage(e.originalEvent.dataTransfer.files[0]);
+ }
+ });
+
+ $dropzone.on('click', function (e) {
+ stopEvent(e);
+ var $input = $('').trigger('click');
+ $input.change(function () {
+ if (this.files.length) {
+ self.uploadImage(this.files[0]);
+ }
+ });
+ });
+
+ this.renderToolbar(fieldModel);
+ break;
+
+ case 'changed':
+ break;
+
+ case 'saving':
+ if (from === 'invalid') {
+ this.removeValidationErrors();
+ }
+
+ this.save(options);
+ break;
+
+ case 'saved':
+ break;
+
+ case 'invalid':
+ this.showValidationErrors();
+ break;
+ }
+ },
+
+ /**
+ * Validates/uploads a given file.
+ *
+ * @param {File} file
+ * The file to upload.
+ */
+ uploadImage: function (file) {
+ // Indicate loading by adding a special class to our icon.
+ this.renderDropzone('upload loading', Drupal.t('Uploading @file...', {'@file': file.name}));
+
+ // Build a valid URL for our endpoint.
+ var fieldID = this.fieldModel.get('fieldID');
+ var url = Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('quickedit/image/upload/!entity_type/!id/!field_name/!langcode/!view_mode'));
+
+ // Construct form data that our endpoint can consume.
+ var data = new FormData();
+ data.append('files[image]', file);
+
+ // Construct a POST request to our endpoint.
+ var self = this;
+ this.ajax('POST', url, data, function (response) {
+ var $el = $(self.fieldModel.get('el'));
+ // Indicate that the field has changed - this enables the
+ // "Save" button.
+ self.fieldModel.set('state', 'changed');
+ 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.
+ var content = $(response.html).closest('[data-quickedit-field-id]').html();
+ $el.html(content);
+ });
+ },
+
+ /**
+ * Utility function to make an AJAX request to the server.
+ *
+ * In addition to formatting the correct request, this also handles error
+ * 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.
+ *
+ * @param {string} type
+ * The type of request (i.e. GET, POST, PUT, DELETE, etc.)
+ * @param {string} url
+ * The URL for the request.
+ * @param {*} data
+ * The data to send to the server.
+ * @param {function} callback
+ * A callback function used when a request is successful, without errors.
+ */
+ ajax: function (type, url, data, callback) {
+ $.ajax({
+ url: url,
+ context: this,
+ type: type,
+ data: data,
+ dataType: 'json',
+ cache: false,
+ contentType: false,
+ processData: false,
+ success: function (response) {
+ if (response.main_error) {
+ this.renderDropzone('error', response.main_error);
+ if (response.errors.length) {
+ this.model.set('validationErrors', response.errors);
+ }
+ this.showValidationErrors();
+ }
+ else {
+ callback(response);
+ }
+ },
+ error: function () {
+ this.renderDropzone('error', Drupal.t('A server error has occurred.'));
+ }
+ });
+ },
+
+ /**
+ * Renders our toolbar form for editing metadata.
+ *
+ * @param {Drupal.quickedit.FieldModel} fieldModel
+ * The current Field Model.
+ */
+ renderToolbar: function (fieldModel) {
+ var $toolgroup = $('#' + fieldModel.toolbarView.getMainWysiwygToolgroupId());
+ var $toolbar = $toolgroup.find('.quickedit-image-field-info');
+ if ($toolbar.length === 0) {
+ // Perform an AJAX request for extra image info (alt/title).
+ var fieldID = fieldModel.get('fieldID');
+ var url = Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('quickedit/image/info/!entity_type/!id/!field_name/!langcode/!view_mode'));
+ var self = this;
+ self.ajax('GET', url, null, function (response) {
+ $toolbar = $(Drupal.theme.quickeditImageToolbar(response));
+ $toolgroup.append($toolbar);
+ $toolbar.on('keyup paste', function () {
+ fieldModel.set('state', 'changed');
+ });
+ // Re-position the toolbar, which could have changed size.
+ fieldModel.get('entity').toolbarView.position();
+ });
+ }
+ },
+
+ /**
+ * Renders our dropzone element.
+ *
+ * @param {string} state
+ * The current state of our editor. Only used for visual styling.
+ * @param {string} text
+ * The text to display in the dropzone area.
+ */
+ renderDropzone: function (state, text) {
+ var $dropzone = this.$el.find('.quickedit-image-dropzone');
+ // If the element already exists, modify its contents.
+ if ($dropzone.length) {
+ $dropzone.removeClass('upload error hover loading');
+ $dropzone.addClass('.quickedit-image-dropzone ' + state);
+ $dropzone.children('.quickedit-image-text').html(text);
+ }
+ else {
+ $dropzone = $(Drupal.theme.quickeditImageDropzone({
+ state: state,
+ text: text
+ }));
+ this.$el.append($dropzone);
+ }
+
+ return $dropzone;
+ },
+
+ /**
+ * @inheritdoc
+ */
+ revert: function () {
+ this.$el.html(this.model.get('originalValue'));
+ },
+
+ /**
+ * @inheritdoc
+ */
+ getQuickEditUISettings: function () {
+ return {padding: false, unifiedToolbar: true, fullWidthToolbar: true, popup: false};
+ },
+
+ /**
+ * @inheritdoc
+ */
+ showValidationErrors: function () {
+ var $errors = $(Drupal.theme.quickeditImageErrors({
+ errors: this.model.get('validationErrors')
+ }));
+ $('#' + this.fieldModel.toolbarView.getMainWysiwygToolgroupId())
+ .append($errors);
+ this.getEditedElement()
+ .addClass('quickedit-validation-error');
+ // Re-position the toolbar, which could have changed size.
+ this.fieldModel.get('entity').toolbarView.position();
+ },
+
+ /**
+ * @inheritdoc
+ */
+ removeValidationErrors: function () {
+ $('#' + this.fieldModel.toolbarView.getMainWysiwygToolgroupId())
+ .find('.quickedit-image-errors').remove();
+ this.getEditedElement()
+ .removeClass('quickedit-validation-error');
+ }
+
+ });
+
+})(jQuery, _, Drupal);
diff --git a/core/modules/image/src/Controller/QuickEditImageController.php b/core/modules/image/src/Controller/QuickEditImageController.php
new file mode 100644
index 0000000..bf98948
--- /dev/null
+++ b/core/modules/image/src/Controller/QuickEditImageController.php
@@ -0,0 +1,208 @@
+renderer = $renderer;
+ $this->imageFactory = $image_factory;
+ $this->tempStore = $temp_store_factory->get('quickedit');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('renderer'),
+ $container->get('image.factory'),
+ $container->get('user.private_tempstore')
+ );
+ }
+
+ /**
+ * Returns a JSON object representing the new file upload, or validation
+ * errors.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity of which an image field is being rendered.
+ * @param string $field_name
+ * The name of the (image) field that is being rendered
+ * @param string $langcode
+ * The language code of the field that is being rendered.
+ * @param string $view_mode_id
+ * The view mode of the field that is being rendered.
+ *
+ * @return \Symfony\Component\HttpFoundation\JsonResponse
+ * The JSON response.
+ */
+ public function upload(EntityInterface $entity, $field_name, $langcode, $view_mode_id) {
+ $field = $this->getField($entity, $field_name, $langcode);
+ $field_validators = $field->getUploadValidators();
+ $field_settings = $field->getFieldDefinition()->getSettings();
+ $destination = $field->getUploadLocation();
+
+ // Add upload resolution validation.
+ if ($field_settings['max_resolution'] || $field_settings['min_resolution']) {
+ $field_validators['file_validate_image_resolution'] = [$field_settings['max_resolution'], $field_settings['min_resolution']];
+ }
+
+ // Create the destination directory if it does not already exist.
+ if (isset($destination) && !file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) {
+ return new JsonResponse(['main_error' => $this->t('The destination directory could not be created.'), 'errors' => '']);
+ }
+
+ // Attempt to save the image given the field's constraints.
+ $result = file_save_upload('image', $field_validators, $destination);
+ if (is_array($result) && $result[0]) {
+ /** @var \Drupal\file\Entity\File $file */
+ $file = $result[0];
+ $image = $this->imageFactory->get($file->getFileUri());
+
+ // Set the value in the Entity to the new file.
+ /** @var \Drupal\file\Plugin\Field\FieldType\FileFieldItemList $field_list */
+ $value = $entity->$field_name->getValue();
+ $value[0]['target_id'] = $file->id();
+ $value[0]['width'] = $image->getWidth();
+ $value[0]['height'] = $image->getHeight();
+ $entity->$field_name->setValue($value);
+
+ // Render the new image using the correct formatter settings.
+ $entity_view_mode_ids = array_keys($this->entityManager()->getViewModes($entity->getEntityTypeId()));
+ if (in_array($view_mode_id, $entity_view_mode_ids)) {
+ $output = $entity->$field_name->view($view_mode_id);
+ }
+ else {
+ // Each part of a custom (non-Entity Display) view mode ID is separated
+ // by a dash; the first part must be the module name.
+ $mode_id_parts = explode('-', $view_mode_id, 2);
+ $module = reset($mode_id_parts);
+ $args = [$entity, $field_name, $view_mode_id, $langcode];
+ $output = $this->moduleHandler()->invoke($module, 'quickedit_render_field', $args);
+ }
+
+ // Save the Entity to tempstore.
+ $this->tempStore->set($entity->uuid(), $entity);
+
+ $data = [
+ 'fid' => $file->id(),
+ 'html' => $this->renderer->renderRoot($output),
+ ];
+ return new JsonResponse($data);
+ }
+ else {
+ // 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.')]);
+ }
+ }
+
+ /**
+ * Returns a JSON object representing an image field's metadata.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity of which an image field is being rendered.
+ * @param string $field_name
+ * The name of the (image) field that is being rendered
+ * @param string $langcode
+ * The language code of the field that is being rendered.
+ * @param string $view_mode_id
+ * The view mode of the field that is being rendered.
+ *
+ * @return \Symfony\Component\HttpFoundation\JsonResponse
+ * The Ajax response.
+ */
+ public function getInfo(EntityInterface $entity, $field_name, $langcode, $view_mode_id) {
+ $field = $this->getField($entity, $field_name, $langcode);
+ $settings = $field->getFieldDefinition()->getSettings();
+ $info = [
+ 'alt' => $field->alt,
+ 'title' => $field->title,
+ 'alt_field' => $settings['alt_field'],
+ 'title_field' => $settings['title_field'],
+ 'alt_field_required' => $settings['alt_field_required'],
+ 'title_field_required' => $settings['title_field_required'],
+ ];
+ return new JsonResponse($info);
+ }
+
+ /**
+ * Returns a JSON object representing the current state of the field.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity of which an image field is being rendered.
+ * @param string $field_name
+ * The name of the (image) field that is being rendered
+ * @param string $langcode
+ * The language code of the field that is being rendered.
+ * @return \Drupal\image\Plugin\Field\FieldType\ImageItem
+ * The field for this request.
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+ * Throws an exception if the request is invalid.
+ */
+ protected function getField(EntityInterface $entity, $field_name, $langcode) {
+ // Ensure that this is a valid Entity.
+ if (!($entity instanceof ContentEntityInterface)) {
+ throw new BadRequestHttpException('Requested Entity is not a Content Entity.');
+ }
+
+ // Get the first item (we only support single-cardinality image fields).
+ $field = $entity->getTranslation($langcode)->$field_name->get(0);
+
+ if (!($field instanceof ImageItem)) {
+ throw new BadRequestHttpException('Requested Field is not of type "image".');
+ }
+
+ return $field;
+ }
+
+}
diff --git a/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php b/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php
index 4c3a27d..c1ee4df 100644
--- a/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php
+++ b/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php
@@ -22,6 +22,9 @@
* label = @Translation("Image"),
* field_types = {
* "image"
+ * },
+ * quickedit = {
+ * "editor" = "image"
* }
* )
*/
diff --git a/core/modules/image/src/Plugin/InPlaceEditor/Image.php b/core/modules/image/src/Plugin/InPlaceEditor/Image.php
new file mode 100644
index 0000000..9cf9cd2
--- /dev/null
+++ b/core/modules/image/src/Plugin/InPlaceEditor/Image.php
@@ -0,0 +1,39 @@
+getFieldDefinition();
+
+ // This editor is only compatible with single-value image fields.
+ return $field_definition->getFieldStorageDefinition()->getCardinality() === 1
+ && $field_definition->getType() === 'image';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAttachments() {
+ return [
+ 'library' => [
+ 'image/quickedit.inPlaceEditor.image',
+ ],
+ ];
+ }
+
+}
diff --git a/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php b/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php
index ff91e68..3ec1a19 100644
--- a/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php
+++ b/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php
@@ -23,6 +23,9 @@
* label = @Translation("Responsive image"),
* field_types = {
* "image",
+ * },
+ * quickedit = {
+ * "editor" = "image"
* }
* )
*/
diff --git a/core/themes/stable/css/image/editors/image.css b/core/themes/stable/css/image/editors/image.css
new file mode 100644
index 0000000..08bb679
--- /dev/null
+++ b/core/themes/stable/css/image/editors/image.css
@@ -0,0 +1,52 @@
+/**
+ * @file
+ * Functional styles for the Image module's in-place editor.
+ */
+
+/**
+ * A minimum width/height is required so that users can drag and drop files
+ * onto small images.
+ */
+.quickedit-image-element {
+ min-width: 200px;
+ min-height: 200px;
+}
+
+.quickedit-image-dropzone {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.quickedit-image-icon {
+ display: block;
+ width: 50px;
+ height: 50px;
+ background-repeat: no-repeat;
+ background-size: cover;
+}
+
+.quickedit-image-field-info {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+.quickedit-image-text {
+ display: block;
+}
+
+/**
+ * If we do not prevent pointer-events for child elements, our drag+drop events
+ * will not fire properly. This can lead to unintentional redirects if a file
+ * is dropped on a child element when a user intended to upload it.
+ */
+.quickedit-image-dropzone * {
+ pointer-events: none;
+}
diff --git a/core/themes/stable/css/image/editors/image.theme.css b/core/themes/stable/css/image/editors/image.theme.css
new file mode 100644
index 0000000..7ff435b
--- /dev/null
+++ b/core/themes/stable/css/image/editors/image.theme.css
@@ -0,0 +1,88 @@
+/**
+ * @file
+ * Theme styles for the Image module's in-place editor.
+ */
+
+.quickedit-image-dropzone {
+ background: rgba(116, 183, 255, 0.8);
+ transition: background .2s;
+}
+
+.quickedit-image-icon {
+ margin: 0 0 10px 0;
+ transition: margin .5s;
+}
+
+.quickedit-image-dropzone.hover {
+ background: rgba(116, 183, 255, 0.9);
+}
+
+.quickedit-image-dropzone.error {
+ background: rgba(255, 52, 27, 0.81);
+}
+
+.quickedit-image-dropzone.upload .quickedit-image-icon {
+ background-image: url('../../../images/image/upload.svg');
+}
+
+.quickedit-image-dropzone.error .quickedit-image-icon {
+ background-image: url('../../../images/image/error.svg');
+}
+
+.quickedit-image-dropzone.loading .quickedit-image-icon {
+ margin: -10px 0 20px 0;
+}
+
+.quickedit-image-dropzone.loading .quickedit-image-icon::after {
+ display: block;
+ content: "";
+ margin-left: -10px;
+ margin-top: -5px;
+ animation-duration: 2s;
+ animation-name: quickedit-image-spin;
+ animation-iteration-count: infinite;
+ animation-timing-function: linear;
+ width: 60px;
+ height: 60px;
+ border-style: solid;
+ border-radius: 35px;
+ border-width: 5px;
+ border-color: white transparent transparent transparent;
+}
+
+@keyframes quickedit-image-spin {
+ 0% {transform: rotate(0deg);}
+ 50% {transform: rotate(180deg);}
+ 100% {transform: rotate(360deg);}
+}
+
+.quickedit-image-text {
+ text-align: center;
+ color: white;
+ font-family: "Droid sans", "Lucida Grande", sans-serif;
+ font-size: 16px;
+ -webkit-user-select: none;
+}
+
+.quickedit-image-field-info {
+ background: rgba(0, 0, 0, 0.05);
+ border-top: 1px solid #c5c5c5;
+ padding: 5px;
+}
+
+.quickedit-image-field-info label, .quickedit-image-field-info input {
+ margin-right: 5px;
+}
+
+.quickedit-image-field-info input:last-child {
+ margin-right: 0;
+}
+
+.quickedit-image-errors .messages__wrapper {
+ margin: 0;
+ padding: 0;
+}
+
+.quickedit-image-errors .messages--error {
+ box-shadow: none;
+}
diff --git a/core/themes/stable/images/image/error.svg b/core/themes/stable/images/image/error.svg
new file mode 100644
index 0000000..1932ea4
--- /dev/null
+++ b/core/themes/stable/images/image/error.svg
@@ -0,0 +1,4 @@
+
diff --git a/core/themes/stable/images/image/upload.svg b/core/themes/stable/images/image/upload.svg
new file mode 100644
index 0000000..168bc43
--- /dev/null
+++ b/core/themes/stable/images/image/upload.svg
@@ -0,0 +1,4 @@
+
diff --git a/core/themes/stable/stable.info.yml b/core/themes/stable/stable.info.yml
index 3476833..7e585b1 100644
--- a/core/themes/stable/stable.info.yml
+++ b/core/themes/stable/stable.info.yml
@@ -98,6 +98,12 @@ libraries-override:
css:
theme:
css/image.admin.css: css/image/image.admin.css
+ image/quickedit.inPlaceEditor.image:
+ css:
+ component:
+ css/editors/image.css: css/image/editors/image.css
+ theme:
+ css/editors/image.theme.css: css/image/editors/image.theme.css
language/drupal.language.admin:
css: