comparison core/modules/image/js/editors/image.es6.js @ 0:c75dbcec494b

Initial commit from drush-created site
author Chris Cannam
date Thu, 05 Jul 2018 14:24:15 +0000
parents
children a9cd425dd02b
comparison
equal deleted inserted replaced
-1:000000000000 0:c75dbcec494b
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));