b43f75f0147f029a34c50b41ec3812d5551e6e06
[yaffs-website] / web / core / modules / image / js / editors / image.es6.js
1 /**
2  * @file
3  * Drag+drop based in-place editor for images.
4  */
5
6 (function($, _, Drupal) {
7   Drupal.quickedit.editors.image = Drupal.quickedit.EditorView.extend(
8     /** @lends Drupal.quickedit.editors.image# */ {
9       /**
10        * @constructs
11        *
12        * @augments Drupal.quickedit.EditorView
13        *
14        * @param {object} options
15        *   Options for the image editor.
16        */
17       initialize(options) {
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)
25             .attr('name')
26             .match(/(alt|title)]$/);
27           if (matches) {
28             const name = matches[1];
29             const $toolgroup = $(
30               `#${options.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`,
31             );
32             const $input = $toolgroup.find(
33               `.quickedit-image-field-info input[name="${name}"]`,
34             );
35             if ($input.length) {
36               return $input.val();
37             }
38           }
39         });
40       },
41
42       /**
43        * @inheritdoc
44        *
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.
51        */
52       stateChange(fieldModel, state, options) {
53         const from = fieldModel.previous('state');
54         switch (state) {
55           case 'inactive':
56             break;
57
58           case 'candidate':
59             if (from !== 'inactive') {
60               this.$el.find('.quickedit-image-dropzone').remove();
61               this.$el.removeClass('quickedit-image-element');
62             }
63             if (from === 'invalid') {
64               this.removeValidationErrors();
65             }
66             break;
67
68           case 'highlighted':
69             break;
70
71           case 'activating':
72             // Defer updating the field model until the current state change has
73             // propagated, to not trigger a nested state change event.
74             _.defer(() => {
75               fieldModel.set('state', 'active');
76             });
77             break;
78
79           case 'active': {
80             const self = this;
81
82             // Indicate that this element is being edited by Quick Edit Image.
83             this.$el.addClass('quickedit-image-element');
84
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(
88               'upload',
89               Drupal.t('Drop file here or click to upload'),
90             );
91
92             $dropzone.on('dragenter', function(e) {
93               $(this).addClass('hover');
94             });
95             $dropzone.on('dragleave', function(e) {
96               $(this).removeClass('hover');
97             });
98
99             $dropzone.on('drop', function(e) {
100               // Only respond when a file is dropped (could be another element).
101               if (
102                 e.originalEvent.dataTransfer &&
103                 e.originalEvent.dataTransfer.files.length
104               ) {
105                 $(this).removeClass('hover');
106                 self.uploadImage(e.originalEvent.dataTransfer.files[0]);
107               }
108             });
109
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">')
115                 .trigger('click')
116                 .on('change', function() {
117                   if (this.files.length) {
118                     self.uploadImage(this.files[0]);
119                   }
120                 });
121             });
122
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 => {
126               e.preventDefault();
127               e.stopPropagation();
128             });
129
130             this.renderToolbar(fieldModel);
131             break;
132           }
133
134           case 'changed':
135             break;
136
137           case 'saving':
138             if (from === 'invalid') {
139               this.removeValidationErrors();
140             }
141
142             this.save(options);
143             break;
144
145           case 'saved':
146             break;
147
148           case 'invalid':
149             this.showValidationErrors();
150             break;
151         }
152       },
153
154       /**
155        * Validates/uploads a given file.
156        *
157        * @param {File} file
158        *   The file to upload.
159        */
160       uploadImage(file) {
161         // Indicate loading by adding a special class to our icon.
162         this.renderDropzone(
163           'upload loading',
164           Drupal.t('Uploading <i>@file</i>…', { '@file': file.name }),
165         );
166
167         // Build a valid URL for our endpoint.
168         const fieldID = this.fieldModel.get('fieldID');
169         const url = Drupal.quickedit.util.buildUrl(
170           fieldID,
171           Drupal.url(
172             'quickedit/image/upload/!entity_type/!id/!field_name/!langcode/!view_mode',
173           ),
174         );
175
176         // Construct form data that our endpoint can consume.
177         const data = new FormData();
178         data.append('files[image]', file);
179
180         // Construct a POST request to our endpoint.
181         const self = this;
182         this.ajax({
183           type: 'POST',
184           url,
185           data,
186           success(response) {
187             const $el = $(self.fieldModel.get('el'));
188             // Indicate that the field has changed - this enables the
189             // "Save" button.
190             self.fieldModel.set('state', 'changed');
191             self.fieldModel.get('entity').set('inTempStore', true);
192             self.removeValidationErrors();
193
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]')
199               .children();
200             $el.empty().append($content);
201           },
202         });
203       },
204
205       /**
206        * Utility function to make an AJAX request to the server.
207        *
208        * In addition to formatting the correct request, this also handles error
209        * codes and messages by displaying them visually inline with the image.
210        *
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.
215        *
216        * @param {object} options
217        *   Ajax 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.
226        */
227       ajax(options) {
228         const defaultOptions = {
229           context: this,
230           dataType: 'json',
231           cache: false,
232           contentType: false,
233           processData: false,
234           error() {
235             this.renderDropzone(
236               'error',
237               Drupal.t('A server error has occurred.'),
238             );
239           },
240         };
241
242         const ajaxOptions = $.extend(defaultOptions, options);
243         const successCallback = ajaxOptions.success;
244
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);
251             }
252             this.showValidationErrors();
253           } else {
254             successCallback(response);
255           }
256         };
257
258         $.ajax(ajaxOptions);
259       },
260
261       /**
262        * Renders our toolbar form for editing metadata.
263        *
264        * @param {Drupal.quickedit.FieldModel} fieldModel
265        *   The current Field Model.
266        */
267       renderToolbar(fieldModel) {
268         const $toolgroup = $(
269           `#${fieldModel.toolbarView.getMainWysiwygToolgroupId()}`,
270         );
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(
276             fieldID,
277             Drupal.url(
278               'quickedit/image/info/!entity_type/!id/!field_name/!langcode/!view_mode',
279             ),
280           );
281           const self = this;
282           self.ajax({
283             type: 'GET',
284             url,
285             success(response) {
286               $toolbar = $(Drupal.theme.quickeditImageToolbar(response));
287               $toolgroup.append($toolbar);
288               $toolbar.on('keyup paste', () => {
289                 fieldModel.set('state', 'changed');
290               });
291               // Re-position the toolbar, which could have changed size.
292               fieldModel.get('entity').toolbarView.position();
293             },
294           });
295         }
296       },
297
298       /**
299        * Renders our dropzone element.
300        *
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.
305        *
306        * @return {jQuery}
307        *   The rendered dropzone.
308        */
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) {
313           $dropzone
314             .removeClass('upload error hover loading')
315             .addClass(`.quickedit-image-dropzone ${state}`)
316             .children('.quickedit-image-text')
317             .html(text);
318         } else {
319           $dropzone = $(
320             Drupal.theme('quickeditImageDropzone', {
321               state,
322               text,
323             }),
324           );
325           this.$el.append($dropzone);
326         }
327
328         return $dropzone;
329       },
330
331       /**
332        * @inheritdoc
333        */
334       revert() {
335         this.$el.html(this.model.get('originalValue'));
336       },
337
338       /**
339        * @inheritdoc
340        */
341       getQuickEditUISettings() {
342         return {
343           padding: false,
344           unifiedToolbar: true,
345           fullWidthToolbar: true,
346           popup: false,
347         };
348       },
349
350       /**
351        * @inheritdoc
352        */
353       showValidationErrors() {
354         const errors = Drupal.theme('quickeditImageErrors', {
355           errors: this.model.get('validationErrors'),
356         });
357         $(`#${this.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`).append(
358           errors,
359         );
360         this.getEditedElement().addClass('quickedit-validation-error');
361         // Re-position the toolbar, which could have changed size.
362         this.fieldModel.get('entity').toolbarView.position();
363       },
364
365       /**
366        * @inheritdoc
367        */
368       removeValidationErrors() {
369         $(`#${this.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`)
370           .find('.quickedit-image-errors')
371           .remove();
372         this.getEditedElement().removeClass('quickedit-validation-error');
373       },
374     },
375   );
376 })(jQuery, _, Drupal);