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