fe0e6f56d65c420fd01c32af055d7979a6dd941a
[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(/** @lends Drupal.quickedit.editors.image# */{
8
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).attr('name').match(/(alt|title)]$/);
25         if (matches) {
26           const name = matches[1];
27           const $toolgroup = $(`#${options.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`);
28           const $input = $toolgroup.find(`.quickedit-image-field-info input[name="${name}"]`);
29           if ($input.length) {
30             return $input.val();
31           }
32         }
33       });
34     },
35
36     /**
37      * @inheritdoc
38      *
39      * @param {Drupal.quickedit.FieldModel} fieldModel
40      *   The field model that holds the state.
41      * @param {string} state
42      *   The state to change to.
43      * @param {object} options
44      *   State options, if needed by the state change.
45      */
46     stateChange(fieldModel, state, options) {
47       const from = fieldModel.previous('state');
48       switch (state) {
49         case 'inactive':
50           break;
51
52         case 'candidate':
53           if (from !== 'inactive') {
54             this.$el.find('.quickedit-image-dropzone').remove();
55             this.$el.removeClass('quickedit-image-element');
56           }
57           if (from === 'invalid') {
58             this.removeValidationErrors();
59           }
60           break;
61
62         case 'highlighted':
63           break;
64
65         case 'activating':
66           // Defer updating the field model until the current state change has
67           // propagated, to not trigger a nested state change event.
68           _.defer(() => {
69             fieldModel.set('state', 'active');
70           });
71           break;
72
73         case 'active':
74           var self = this;
75
76           // Indicate that this element is being edited by Quick Edit Image.
77           this.$el.addClass('quickedit-image-element');
78
79           // Render our initial dropzone element. Once the user reverts changes
80           // or saves a new image, this element is removed.
81           var $dropzone = this.renderDropzone('upload', Drupal.t('Drop file here or click to upload'));
82
83           $dropzone.on('dragenter', function (e) {
84             $(this).addClass('hover');
85           });
86           $dropzone.on('dragleave', function (e) {
87             $(this).removeClass('hover');
88           });
89
90           $dropzone.on('drop', function (e) {
91             // Only respond when a file is dropped (could be another element).
92             if (e.originalEvent.dataTransfer && e.originalEvent.dataTransfer.files.length) {
93               $(this).removeClass('hover');
94               self.uploadImage(e.originalEvent.dataTransfer.files[0]);
95             }
96           });
97
98           $dropzone.on('click', (e) => {
99             // Create an <input> element without appending it to the DOM, and
100             // trigger a click event. This is the easiest way to arbitrarily
101             // open the browser's upload dialog.
102             $('<input type="file">')
103               .trigger('click')
104               .on('change', function () {
105                 if (this.files.length) {
106                   self.uploadImage(this.files[0]);
107                 }
108               });
109           });
110
111           // Prevent the browser's default behavior when dragging files onto
112           // the document (usually opens them in the same tab).
113           $dropzone.on('dragover dragenter dragleave drop click', (e) => {
114             e.preventDefault();
115             e.stopPropagation();
116           });
117
118           this.renderToolbar(fieldModel);
119           break;
120
121         case 'changed':
122           break;
123
124         case 'saving':
125           if (from === 'invalid') {
126             this.removeValidationErrors();
127           }
128
129           this.save(options);
130           break;
131
132         case 'saved':
133           break;
134
135         case 'invalid':
136           this.showValidationErrors();
137           break;
138       }
139     },
140
141     /**
142      * Validates/uploads a given file.
143      *
144      * @param {File} file
145      *   The file to upload.
146      */
147     uploadImage(file) {
148       // Indicate loading by adding a special class to our icon.
149       this.renderDropzone('upload loading', Drupal.t('Uploading <i>@file</i>…', { '@file': file.name }));
150
151       // Build a valid URL for our endpoint.
152       const fieldID = this.fieldModel.get('fieldID');
153       const url = Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('quickedit/image/upload/!entity_type/!id/!field_name/!langcode/!view_mode'));
154
155       // Construct form data that our endpoint can consume.
156       const data = new FormData();
157       data.append('files[image]', file);
158
159       // Construct a POST request to our endpoint.
160       const self = this;
161       this.ajax({
162         type: 'POST',
163         url,
164         data,
165         success(response) {
166           const $el = $(self.fieldModel.get('el'));
167           // Indicate that the field has changed - this enables the
168           // "Save" button.
169           self.fieldModel.set('state', 'changed');
170           self.fieldModel.get('entity').set('inTempStore', true);
171           self.removeValidationErrors();
172
173           // Replace our html with the new image. If we replaced our entire
174           // element with data.html, we would have to implement complicated logic
175           // like what's in Drupal.quickedit.AppView.renderUpdatedField.
176           const $content = $(response.html).closest('[data-quickedit-field-id]').children();
177           $el.empty().append($content);
178         },
179       });
180     },
181
182     /**
183      * Utility function to make an AJAX request to the server.
184      *
185      * In addition to formatting the correct request, this also handles error
186      * codes and messages by displaying them visually inline with the image.
187      *
188      * Drupal.ajax is not called here as the Form API is unused by this
189      * in-place editor, and our JSON requests/responses try to be
190      * editor-agnostic. Ideally similar logic and routes could be used by
191      * modules like CKEditor for drag+drop file uploads as well.
192      *
193      * @param {object} options
194      *   Ajax options.
195      * @param {string} options.type
196      *   The type of request (i.e. GET, POST, PUT, DELETE, etc.)
197      * @param {string} options.url
198      *   The URL for the request.
199      * @param {*} options.data
200      *   The data to send to the server.
201      * @param {function} options.success
202      *   A callback function used when a request is successful, without errors.
203      */
204     ajax(options) {
205       const defaultOptions = {
206         context: this,
207         dataType: 'json',
208         cache: false,
209         contentType: false,
210         processData: false,
211         error() {
212           this.renderDropzone('error', Drupal.t('A server error has occurred.'));
213         },
214       };
215
216       const ajaxOptions = $.extend(defaultOptions, options);
217       const successCallback = ajaxOptions.success;
218
219       // Handle the success callback.
220       ajaxOptions.success = function (response) {
221         if (response.main_error) {
222           this.renderDropzone('error', response.main_error);
223           if (response.errors.length) {
224             this.model.set('validationErrors', response.errors);
225           }
226           this.showValidationErrors();
227         }
228         else {
229           successCallback(response);
230         }
231       };
232
233       $.ajax(ajaxOptions);
234     },
235
236     /**
237      * Renders our toolbar form for editing metadata.
238      *
239      * @param {Drupal.quickedit.FieldModel} fieldModel
240      *   The current Field Model.
241      */
242     renderToolbar(fieldModel) {
243       const $toolgroup = $(`#${fieldModel.toolbarView.getMainWysiwygToolgroupId()}`);
244       let $toolbar = $toolgroup.find('.quickedit-image-field-info');
245       if ($toolbar.length === 0) {
246         // Perform an AJAX request for extra image info (alt/title).
247         const fieldID = fieldModel.get('fieldID');
248         const url = Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('quickedit/image/info/!entity_type/!id/!field_name/!langcode/!view_mode'));
249         const self = this;
250         self.ajax({
251           type: 'GET',
252           url,
253           success(response) {
254             $toolbar = $(Drupal.theme.quickeditImageToolbar(response));
255             $toolgroup.append($toolbar);
256             $toolbar.on('keyup paste', () => {
257               fieldModel.set('state', 'changed');
258             });
259             // Re-position the toolbar, which could have changed size.
260             fieldModel.get('entity').toolbarView.position();
261           },
262         });
263       }
264     },
265
266     /**
267      * Renders our dropzone element.
268      *
269      * @param {string} state
270      *   The current state of our editor. Only used for visual styling.
271      * @param {string} text
272      *   The text to display in the dropzone area.
273      *
274      * @return {jQuery}
275      *   The rendered dropzone.
276      */
277     renderDropzone(state, text) {
278       let $dropzone = this.$el.find('.quickedit-image-dropzone');
279       // If the element already exists, modify its contents.
280       if ($dropzone.length) {
281         $dropzone
282           .removeClass('upload error hover loading')
283           .addClass(`.quickedit-image-dropzone ${state}`)
284           .children('.quickedit-image-text')
285             .html(text);
286       }
287       else {
288         $dropzone = $(Drupal.theme('quickeditImageDropzone', {
289           state,
290           text,
291         }));
292         this.$el.append($dropzone);
293       }
294
295       return $dropzone;
296     },
297
298     /**
299      * @inheritdoc
300      */
301     revert() {
302       this.$el.html(this.model.get('originalValue'));
303     },
304
305     /**
306      * @inheritdoc
307      */
308     getQuickEditUISettings() {
309       return { padding: false, unifiedToolbar: true, fullWidthToolbar: true, popup: false };
310     },
311
312     /**
313      * @inheritdoc
314      */
315     showValidationErrors() {
316       const errors = Drupal.theme('quickeditImageErrors', {
317         errors: this.model.get('validationErrors'),
318       });
319       $(`#${this.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`)
320         .append(errors);
321       this.getEditedElement()
322         .addClass('quickedit-validation-error');
323       // Re-position the toolbar, which could have changed size.
324       this.fieldModel.get('entity').toolbarView.position();
325     },
326
327     /**
328      * @inheritdoc
329      */
330     removeValidationErrors() {
331       $(`#${this.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`)
332         .find('.quickedit-image-errors').remove();
333       this.getEditedElement()
334         .removeClass('quickedit-validation-error');
335     },
336
337   });
338 }(jQuery, _, Drupal));