3 * Drag+drop based in-place editor for images.
6 (function($, _, Drupal) {
7 Drupal.quickedit.editors.image = Drupal.quickedit.EditorView.extend(
8 /** @lends Drupal.quickedit.editors.image# */ {
12 * @augments Drupal.quickedit.EditorView
14 * @param {object} options
15 * Options for the image editor.
18 Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
19 // Set our original value to our current HTML (for reverting).
20 this.model.set('originalValue', this.$el.html().trim());
21 // $.val() callback function for copying input from our custom form to
22 // the Quick Edit Field Form.
23 this.model.set('currentValue', function(index, value) {
24 const matches = $(this)
26 .match(/(alt|title)]$/);
28 const name = matches[1];
30 `#${options.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`,
32 const $input = $toolgroup.find(
33 `.quickedit-image-field-info input[name="${name}"]`,
45 * @param {Drupal.quickedit.FieldModel} fieldModel
46 * The field model that holds the state.
47 * @param {string} state
48 * The state to change to.
49 * @param {object} options
50 * State options, if needed by the state change.
52 stateChange(fieldModel, state, options) {
53 const from = fieldModel.previous('state');
59 if (from !== 'inactive') {
60 this.$el.find('.quickedit-image-dropzone').remove();
61 this.$el.removeClass('quickedit-image-element');
63 if (from === 'invalid') {
64 this.removeValidationErrors();
72 // Defer updating the field model until the current state change has
73 // propagated, to not trigger a nested state change event.
75 fieldModel.set('state', 'active');
82 // Indicate that this element is being edited by Quick Edit Image.
83 this.$el.addClass('quickedit-image-element');
85 // Render our initial dropzone element. Once the user reverts changes
86 // or saves a new image, this element is removed.
87 const $dropzone = this.renderDropzone(
89 Drupal.t('Drop file here or click to upload'),
92 $dropzone.on('dragenter', function(e) {
93 $(this).addClass('hover');
95 $dropzone.on('dragleave', function(e) {
96 $(this).removeClass('hover');
99 $dropzone.on('drop', function(e) {
100 // Only respond when a file is dropped (could be another element).
102 e.originalEvent.dataTransfer &&
103 e.originalEvent.dataTransfer.files.length
105 $(this).removeClass('hover');
106 self.uploadImage(e.originalEvent.dataTransfer.files[0]);
110 $dropzone.on('click', e => {
111 // Create an <input> element without appending it to the DOM, and
112 // trigger a click event. This is the easiest way to arbitrarily
113 // open the browser's upload dialog.
114 $('<input type="file">')
116 .on('change', function() {
117 if (this.files.length) {
118 self.uploadImage(this.files[0]);
123 // Prevent the browser's default behavior when dragging files onto
124 // the document (usually opens them in the same tab).
125 $dropzone.on('dragover dragenter dragleave drop click', e => {
130 this.renderToolbar(fieldModel);
138 if (from === 'invalid') {
139 this.removeValidationErrors();
149 this.showValidationErrors();
155 * Validates/uploads a given file.
158 * The file to upload.
161 // Indicate loading by adding a special class to our icon.
164 Drupal.t('Uploading <i>@file</i>…', { '@file': file.name }),
167 // Build a valid URL for our endpoint.
168 const fieldID = this.fieldModel.get('fieldID');
169 const url = Drupal.quickedit.util.buildUrl(
172 'quickedit/image/upload/!entity_type/!id/!field_name/!langcode/!view_mode',
176 // Construct form data that our endpoint can consume.
177 const data = new FormData();
178 data.append('files[image]', file);
180 // Construct a POST request to our endpoint.
187 const $el = $(self.fieldModel.get('el'));
188 // Indicate that the field has changed - this enables the
190 self.fieldModel.set('state', 'changed');
191 self.fieldModel.get('entity').set('inTempStore', true);
192 self.removeValidationErrors();
194 // Replace our html with the new image. If we replaced our entire
195 // element with data.html, we would have to implement complicated logic
196 // like what's in Drupal.quickedit.AppView.renderUpdatedField.
197 const $content = $(response.html)
198 .closest('[data-quickedit-field-id]')
200 $el.empty().append($content);
206 * Utility function to make an AJAX request to the server.
208 * In addition to formatting the correct request, this also handles error
209 * codes and messages by displaying them visually inline with the image.
211 * Drupal.ajax is not called here as the Form API is unused by this
212 * in-place editor, and our JSON requests/responses try to be
213 * editor-agnostic. Ideally similar logic and routes could be used by
214 * modules like CKEditor for drag+drop file uploads as well.
216 * @param {object} options
218 * @param {string} options.type
219 * The type of request (i.e. GET, POST, PUT, DELETE, etc.)
220 * @param {string} options.url
221 * The URL for the request.
222 * @param {*} options.data
223 * The data to send to the server.
224 * @param {function} options.success
225 * A callback function used when a request is successful, without errors.
228 const defaultOptions = {
237 Drupal.t('A server error has occurred.'),
242 const ajaxOptions = $.extend(defaultOptions, options);
243 const successCallback = ajaxOptions.success;
245 // Handle the success callback.
246 ajaxOptions.success = function(response) {
247 if (response.main_error) {
248 this.renderDropzone('error', response.main_error);
249 if (response.errors.length) {
250 this.model.set('validationErrors', response.errors);
252 this.showValidationErrors();
254 successCallback(response);
262 * Renders our toolbar form for editing metadata.
264 * @param {Drupal.quickedit.FieldModel} fieldModel
265 * The current Field Model.
267 renderToolbar(fieldModel) {
268 const $toolgroup = $(
269 `#${fieldModel.toolbarView.getMainWysiwygToolgroupId()}`,
271 let $toolbar = $toolgroup.find('.quickedit-image-field-info');
272 if ($toolbar.length === 0) {
273 // Perform an AJAX request for extra image info (alt/title).
274 const fieldID = fieldModel.get('fieldID');
275 const url = Drupal.quickedit.util.buildUrl(
278 'quickedit/image/info/!entity_type/!id/!field_name/!langcode/!view_mode',
286 $toolbar = $(Drupal.theme.quickeditImageToolbar(response));
287 $toolgroup.append($toolbar);
288 $toolbar.on('keyup paste', () => {
289 fieldModel.set('state', 'changed');
291 // Re-position the toolbar, which could have changed size.
292 fieldModel.get('entity').toolbarView.position();
299 * Renders our dropzone element.
301 * @param {string} state
302 * The current state of our editor. Only used for visual styling.
303 * @param {string} text
304 * The text to display in the dropzone area.
307 * The rendered dropzone.
309 renderDropzone(state, text) {
310 let $dropzone = this.$el.find('.quickedit-image-dropzone');
311 // If the element already exists, modify its contents.
312 if ($dropzone.length) {
314 .removeClass('upload error hover loading')
315 .addClass(`.quickedit-image-dropzone ${state}`)
316 .children('.quickedit-image-text')
320 Drupal.theme('quickeditImageDropzone', {
325 this.$el.append($dropzone);
335 this.$el.html(this.model.get('originalValue'));
341 getQuickEditUISettings() {
344 unifiedToolbar: true,
345 fullWidthToolbar: true,
353 showValidationErrors() {
354 const errors = Drupal.theme('quickeditImageErrors', {
355 errors: this.model.get('validationErrors'),
357 $(`#${this.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`).append(
360 this.getEditedElement().addClass('quickedit-validation-error');
361 // Re-position the toolbar, which could have changed size.
362 this.fieldModel.get('entity').toolbarView.position();
368 removeValidationErrors() {
369 $(`#${this.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`)
370 .find('.quickedit-image-errors')
372 this.getEditedElement().removeClass('quickedit-validation-error');
376 })(jQuery, _, Drupal);