Mercurial > hg > isophonics-drupal-site
view core/modules/image/js/editors/image.es6.js @ 17:129ea1e6d783
Update, including to Drupal core 8.6.10
author | Chris Cannam |
---|---|
date | Thu, 28 Feb 2019 13:21:36 +0000 |
parents | 1fec387a4317 |
children |
line wrap: on
line source
/** * @file * Drag+drop based in-place editor for images. */ (function($, _, Drupal) { Drupal.quickedit.editors.image = Drupal.quickedit.EditorView.extend( /** @lends Drupal.quickedit.editors.image# */ { /** * @constructs * * @augments Drupal.quickedit.EditorView * * @param {object} options * Options for the image editor. */ initialize(options) { Drupal.quickedit.EditorView.prototype.initialize.call(this, options); // Set our original value to our current HTML (for reverting). this.model.set('originalValue', this.$el.html().trim()); // $.val() callback function for copying input from our custom form to // the Quick Edit Field Form. this.model.set('currentValue', function(index, value) { const matches = $(this) .attr('name') .match(/(alt|title)]$/); if (matches) { const name = matches[1]; const $toolgroup = $( `#${options.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`, ); const $input = $toolgroup.find( `.quickedit-image-field-info input[name="${name}"]`, ); if ($input.length) { return $input.val(); } } }); }, /** * @inheritdoc * * @param {Drupal.quickedit.FieldModel} fieldModel * The field model that holds the state. * @param {string} state * The state to change to. * @param {object} options * State options, if needed by the state change. */ stateChange(fieldModel, state, options) { const from = fieldModel.previous('state'); switch (state) { case 'inactive': break; case 'candidate': if (from !== 'inactive') { this.$el.find('.quickedit-image-dropzone').remove(); this.$el.removeClass('quickedit-image-element'); } if (from === 'invalid') { this.removeValidationErrors(); } break; case 'highlighted': break; case 'activating': // Defer updating the field model until the current state change has // propagated, to not trigger a nested state change event. _.defer(() => { fieldModel.set('state', 'active'); }); break; case 'active': { const self = this; // Indicate that this element is being edited by Quick Edit Image. this.$el.addClass('quickedit-image-element'); // Render our initial dropzone element. Once the user reverts changes // or saves a new image, this element is removed. const $dropzone = this.renderDropzone( 'upload', Drupal.t('Drop file here or click to upload'), ); $dropzone.on('dragenter', function(e) { $(this).addClass('hover'); }); $dropzone.on('dragleave', function(e) { $(this).removeClass('hover'); }); $dropzone.on('drop', function(e) { // Only respond when a file is dropped (could be another element). if ( e.originalEvent.dataTransfer && e.originalEvent.dataTransfer.files.length ) { $(this).removeClass('hover'); self.uploadImage(e.originalEvent.dataTransfer.files[0]); } }); $dropzone.on('click', e => { // Create an <input> element without appending it to the DOM, and // trigger a click event. This is the easiest way to arbitrarily // open the browser's upload dialog. $('<input type="file">') .trigger('click') .on('change', function() { if (this.files.length) { self.uploadImage(this.files[0]); } }); }); // Prevent the browser's default behavior when dragging files onto // the document (usually opens them in the same tab). $dropzone.on('dragover dragenter dragleave drop click', e => { e.preventDefault(); e.stopPropagation(); }); this.renderToolbar(fieldModel); break; } case 'changed': break; case 'saving': if (from === 'invalid') { this.removeValidationErrors(); } this.save(options); break; case 'saved': break; case 'invalid': this.showValidationErrors(); break; } }, /** * Validates/uploads a given file. * * @param {File} file * The file to upload. */ uploadImage(file) { // Indicate loading by adding a special class to our icon. this.renderDropzone( 'upload loading', Drupal.t('Uploading <i>@file</i>…', { '@file': file.name }), ); // Build a valid URL for our endpoint. const fieldID = this.fieldModel.get('fieldID'); const url = Drupal.quickedit.util.buildUrl( fieldID, Drupal.url( 'quickedit/image/upload/!entity_type/!id/!field_name/!langcode/!view_mode', ), ); // Construct form data that our endpoint can consume. const data = new FormData(); data.append('files[image]', file); // Construct a POST request to our endpoint. const self = this; this.ajax({ type: 'POST', url, data, success(response) { const $el = $(self.fieldModel.get('el')); // Indicate that the field has changed - this enables the // "Save" button. self.fieldModel.set('state', 'changed'); self.fieldModel.get('entity').set('inTempStore', true); self.removeValidationErrors(); // Replace our html with the new image. If we replaced our entire // element with data.html, we would have to implement complicated logic // like what's in Drupal.quickedit.AppView.renderUpdatedField. const $content = $(response.html) .closest('[data-quickedit-field-id]') .children(); $el.empty().append($content); }, }); }, /** * Utility function to make an AJAX request to the server. * * In addition to formatting the correct request, this also handles error * codes and messages by displaying them visually inline with the image. * * Drupal.ajax is not called here as the Form API is unused by this * in-place editor, and our JSON requests/responses try to be * editor-agnostic. Ideally similar logic and routes could be used by * modules like CKEditor for drag+drop file uploads as well. * * @param {object} options * Ajax options. * @param {string} options.type * The type of request (i.e. GET, POST, PUT, DELETE, etc.) * @param {string} options.url * The URL for the request. * @param {*} options.data * The data to send to the server. * @param {function} options.success * A callback function used when a request is successful, without errors. */ ajax(options) { const defaultOptions = { context: this, dataType: 'json', cache: false, contentType: false, processData: false, error() { this.renderDropzone( 'error', Drupal.t('A server error has occurred.'), ); }, }; const ajaxOptions = $.extend(defaultOptions, options); const successCallback = ajaxOptions.success; // Handle the success callback. ajaxOptions.success = function(response) { if (response.main_error) { this.renderDropzone('error', response.main_error); if (response.errors.length) { this.model.set('validationErrors', response.errors); } this.showValidationErrors(); } else { successCallback(response); } }; $.ajax(ajaxOptions); }, /** * Renders our toolbar form for editing metadata. * * @param {Drupal.quickedit.FieldModel} fieldModel * The current Field Model. */ renderToolbar(fieldModel) { const $toolgroup = $( `#${fieldModel.toolbarView.getMainWysiwygToolgroupId()}`, ); let $toolbar = $toolgroup.find('.quickedit-image-field-info'); if ($toolbar.length === 0) { // Perform an AJAX request for extra image info (alt/title). const fieldID = fieldModel.get('fieldID'); const url = Drupal.quickedit.util.buildUrl( fieldID, Drupal.url( 'quickedit/image/info/!entity_type/!id/!field_name/!langcode/!view_mode', ), ); const self = this; self.ajax({ type: 'GET', url, success(response) { $toolbar = $(Drupal.theme.quickeditImageToolbar(response)); $toolgroup.append($toolbar); $toolbar.on('keyup paste', () => { fieldModel.set('state', 'changed'); }); // Re-position the toolbar, which could have changed size. fieldModel.get('entity').toolbarView.position(); }, }); } }, /** * Renders our dropzone element. * * @param {string} state * The current state of our editor. Only used for visual styling. * @param {string} text * The text to display in the dropzone area. * * @return {jQuery} * The rendered dropzone. */ renderDropzone(state, text) { let $dropzone = this.$el.find('.quickedit-image-dropzone'); // If the element already exists, modify its contents. if ($dropzone.length) { $dropzone .removeClass('upload error hover loading') .addClass(`.quickedit-image-dropzone ${state}`) .children('.quickedit-image-text') .html(text); } else { $dropzone = $( Drupal.theme('quickeditImageDropzone', { state, text, }), ); this.$el.append($dropzone); } return $dropzone; }, /** * @inheritdoc */ revert() { this.$el.html(this.model.get('originalValue')); }, /** * @inheritdoc */ getQuickEditUISettings() { return { padding: false, unifiedToolbar: true, fullWidthToolbar: true, popup: false, }; }, /** * @inheritdoc */ showValidationErrors() { const errors = Drupal.theme('quickeditImageErrors', { errors: this.model.get('validationErrors'), }); $(`#${this.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`).append( errors, ); this.getEditedElement().addClass('quickedit-validation-error'); // Re-position the toolbar, which could have changed size. this.fieldModel.get('entity').toolbarView.position(); }, /** * @inheritdoc */ removeValidationErrors() { $(`#${this.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`) .find('.quickedit-image-errors') .remove(); this.getEditedElement().removeClass('quickedit-validation-error'); }, }, ); })(jQuery, _, Drupal);