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