3 * Drag+drop based in-place editor for images.
6 (function ($, _, Drupal) {
10 Drupal.quickedit.editors.image = Drupal.quickedit.EditorView.extend(/** @lends Drupal.quickedit.editors.image# */{
15 * @augments Drupal.quickedit.EditorView
17 * @param {object} options
18 * Options for the image editor.
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)]$/);
29 var name = matches[1];
30 var $toolgroup = $('#' + options.fieldModel.toolbarView.getMainWysiwygToolgroupId());
31 var $input = $toolgroup.find('.quickedit-image-field-info input[name="' + name + '"]');
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.
49 stateChange: function (fieldModel, state, options) {
50 var from = fieldModel.previous('state');
56 if (from !== 'inactive') {
57 this.$el.find('.quickedit-image-dropzone').remove();
58 this.$el.removeClass('quickedit-image-element');
60 if (from === 'invalid') {
61 this.removeValidationErrors();
69 // Defer updating the field model until the current state change has
70 // propagated, to not trigger a nested state change event.
72 fieldModel.set('state', 'active');
79 // Indicate that this element is being edited by Quick Edit Image.
80 this.$el.addClass('quickedit-image-element');
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'));
86 $dropzone.on('dragenter', function (e) {
87 $(this).addClass('hover');
89 $dropzone.on('dragleave', function (e) {
90 $(this).removeClass('hover');
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]);
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">')
107 .on('change', function () {
108 if (this.files.length) {
109 self.uploadImage(this.files[0]);
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) {
121 this.renderToolbar(fieldModel);
128 if (from === 'invalid') {
129 this.removeValidationErrors();
139 this.showValidationErrors();
145 * Validates/uploads a given file.
148 * The file to upload.
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}));
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'));
158 // Construct form data that our endpoint can consume.
159 var data = new FormData();
160 data.append('files[image]', file);
162 // Construct a POST request to our endpoint.
168 success: function (response) {
169 var $el = $(self.fieldModel.get('el'));
170 // Indicate that the field has changed - this enables the
172 self.fieldModel.set('state', 'changed');
173 self.fieldModel.get('entity').set('inTempStore', true);
174 self.removeValidationErrors();
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);
186 * Utility function to make an AJAX request to the server.
188 * In addition to formatting the correct request, this also handles error
189 * codes and messages by displaying them visually inline with the image.
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.
196 * @param {object} 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.
207 ajax: function (options) {
208 var defaultOptions = {
215 this.renderDropzone('error', Drupal.t('A server error has occurred.'));
219 var ajaxOptions = $.extend(defaultOptions, options);
220 var successCallback = ajaxOptions.success;
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);
229 this.showValidationErrors();
232 successCallback(response);
240 * Renders our toolbar form for editing metadata.
242 * @param {Drupal.quickedit.FieldModel} fieldModel
243 * The current Field Model.
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'));
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');
262 // Re-position the toolbar, which could have changed size.
263 fieldModel.get('entity').toolbarView.position();
270 * Renders our dropzone element.
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.
278 * The rendered dropzone.
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) {
285 .removeClass('upload error hover loading')
286 .addClass('.quickedit-image-dropzone ' + state)
287 .children('.quickedit-image-text')
291 $dropzone = $(Drupal.theme('quickeditImageDropzone', {
295 this.$el.append($dropzone);
304 revert: function () {
305 this.$el.html(this.model.get('originalValue'));
311 getQuickEditUISettings: function () {
312 return {padding: false, unifiedToolbar: true, fullWidthToolbar: true, popup: false};
318 showValidationErrors: function () {
319 var errors = Drupal.theme('quickeditImageErrors', {
320 errors: this.model.get('validationErrors')
322 $('#' + this.fieldModel.toolbarView.getMainWysiwygToolgroupId())
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();
333 removeValidationErrors: function () {
334 $('#' + this.fieldModel.toolbarView.getMainWysiwygToolgroupId())
335 .find('.quickedit-image-errors').remove();
336 this.getEditedElement()
337 .removeClass('quickedit-validation-error');
342 })(jQuery, _, Drupal);