annotate core/modules/image/js/editors/image.es6.js @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
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);