3621ff97aca27632ef8f66e645e3d4ffc8f19718
[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           const 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           const $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
122         case 'changed':
123           break;
124
125         case 'saving':
126           if (from === 'invalid') {
127             this.removeValidationErrors();
128           }
129
130           this.save(options);
131           break;
132
133         case 'saved':
134           break;
135
136         case 'invalid':
137           this.showValidationErrors();
138           break;
139       }
140     },
141
142     /**
143      * Validates/uploads a given file.
144      *
145      * @param {File} file
146      *   The file to upload.
147      */
148     uploadImage(file) {
149       // Indicate loading by adding a special class to our icon.
150       this.renderDropzone('upload loading', Drupal.t('Uploading <i>@file</i>…', { '@file': file.name }));
151
152       // Build a valid URL for our endpoint.
153       const fieldID = this.fieldModel.get('fieldID');
154       const url = Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('quickedit/image/upload/!entity_type/!id/!field_name/!langcode/!view_mode'));
155
156       // Construct form data that our endpoint can consume.
157       const data = new FormData();
158       data.append('files[image]', file);
159
160       // Construct a POST request to our endpoint.
161       const self = this;
162       this.ajax({
163         type: 'POST',
164         url,
165         data,
166         success(response) {
167           const $el = $(self.fieldModel.get('el'));
168           // Indicate that the field has changed - this enables the
169           // "Save" button.
170           self.fieldModel.set('state', 'changed');
171           self.fieldModel.get('entity').set('inTempStore', true);
172           self.removeValidationErrors();
173
174           // Replace our html with the new image. If we replaced our entire
175           // element with data.html, we would have to implement complicated logic
176           // like what's in Drupal.quickedit.AppView.renderUpdatedField.
177           const $content = $(response.html).closest('[data-quickedit-field-id]').children();
178           $el.empty().append($content);
179         },
180       });
181     },
182
183     /**
184      * Utility function to make an AJAX request to the server.
185      *
186      * In addition to formatting the correct request, this also handles error
187      * codes and messages by displaying them visually inline with the image.
188      *
189      * Drupal.ajax is not called here as the Form API is unused by this
190      * in-place editor, and our JSON requests/responses try to be
191      * editor-agnostic. Ideally similar logic and routes could be used by
192      * modules like CKEditor for drag+drop file uploads as well.
193      *
194      * @param {object} options
195      *   Ajax options.
196      * @param {string} options.type
197      *   The type of request (i.e. GET, POST, PUT, DELETE, etc.)
198      * @param {string} options.url
199      *   The URL for the request.
200      * @param {*} options.data
201      *   The data to send to the server.
202      * @param {function} options.success
203      *   A callback function used when a request is successful, without errors.
204      */
205     ajax(options) {
206       const defaultOptions = {
207         context: this,
208         dataType: 'json',
209         cache: false,
210         contentType: false,
211         processData: false,
212         error() {
213           this.renderDropzone('error', Drupal.t('A server error has occurred.'));
214         },
215       };
216
217       const ajaxOptions = $.extend(defaultOptions, options);
218       const successCallback = ajaxOptions.success;
219
220       // Handle the success callback.
221       ajaxOptions.success = function (response) {
222         if (response.main_error) {
223           this.renderDropzone('error', response.main_error);
224           if (response.errors.length) {
225             this.model.set('validationErrors', response.errors);
226           }
227           this.showValidationErrors();
228         }
229         else {
230           successCallback(response);
231         }
232       };
233
234       $.ajax(ajaxOptions);
235     },
236
237     /**
238      * Renders our toolbar form for editing metadata.
239      *
240      * @param {Drupal.quickedit.FieldModel} fieldModel
241      *   The current Field Model.
242      */
243     renderToolbar(fieldModel) {
244       const $toolgroup = $(`#${fieldModel.toolbarView.getMainWysiwygToolgroupId()}`);
245       let $toolbar = $toolgroup.find('.quickedit-image-field-info');
246       if ($toolbar.length === 0) {
247         // Perform an AJAX request for extra image info (alt/title).
248         const fieldID = fieldModel.get('fieldID');
249         const url = Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('quickedit/image/info/!entity_type/!id/!field_name/!langcode/!view_mode'));
250         const self = this;
251         self.ajax({
252           type: 'GET',
253           url,
254           success(response) {
255             $toolbar = $(Drupal.theme.quickeditImageToolbar(response));
256             $toolgroup.append($toolbar);
257             $toolbar.on('keyup paste', () => {
258               fieldModel.set('state', 'changed');
259             });
260             // Re-position the toolbar, which could have changed size.
261             fieldModel.get('entity').toolbarView.position();
262           },
263         });
264       }
265     },
266
267     /**
268      * Renders our dropzone element.
269      *
270      * @param {string} state
271      *   The current state of our editor. Only used for visual styling.
272      * @param {string} text
273      *   The text to display in the dropzone area.
274      *
275      * @return {jQuery}
276      *   The rendered dropzone.
277      */
278     renderDropzone(state, text) {
279       let $dropzone = this.$el.find('.quickedit-image-dropzone');
280       // If the element already exists, modify its contents.
281       if ($dropzone.length) {
282         $dropzone
283           .removeClass('upload error hover loading')
284           .addClass(`.quickedit-image-dropzone ${state}`)
285           .children('.quickedit-image-text')
286             .html(text);
287       }
288       else {
289         $dropzone = $(Drupal.theme('quickeditImageDropzone', {
290           state,
291           text,
292         }));
293         this.$el.append($dropzone);
294       }
295
296       return $dropzone;
297     },
298
299     /**
300      * @inheritdoc
301      */
302     revert() {
303       this.$el.html(this.model.get('originalValue'));
304     },
305
306     /**
307      * @inheritdoc
308      */
309     getQuickEditUISettings() {
310       return { padding: false, unifiedToolbar: true, fullWidthToolbar: true, popup: false };
311     },
312
313     /**
314      * @inheritdoc
315      */
316     showValidationErrors() {
317       const errors = Drupal.theme('quickeditImageErrors', {
318         errors: this.model.get('validationErrors'),
319       });
320       $(`#${this.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`)
321         .append(errors);
322       this.getEditedElement()
323         .addClass('quickedit-validation-error');
324       // Re-position the toolbar, which could have changed size.
325       this.fieldModel.get('entity').toolbarView.position();
326     },
327
328     /**
329      * @inheritdoc
330      */
331     removeValidationErrors() {
332       $(`#${this.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`)
333         .find('.quickedit-image-errors').remove();
334       this.getEditedElement()
335         .removeClass('quickedit-validation-error');
336     },
337
338   });
339 }(jQuery, _, Drupal));