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