annotate 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
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@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));