* 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();
+(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();
+ }
}
- break;
-
- case 'highlighted':
- break;
+ });
+ },
+
+ /**
+ * @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 '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 'highlighted':
+ break;
- case 'active': {
- const self = this;
+ 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;
- // Indicate that this element is being edited by Quick Edit Image.
- this.$el.addClass('quickedit-image-element');
+ case 'active': {
+ const self = this;
- // 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'));
+ // Indicate that this element is being edited by Quick Edit Image.
+ this.$el.addClass('quickedit-image-element');
- $dropzone.on('dragenter', function (e) {
- $(this).addClass('hover');
- });
- $dropzone.on('dragleave', function (e) {
- $(this).removeClass('hover');
- });
+ // 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('drop', function (e) {
- // Only respond when a file is dropped (could be another element).
- if (e.originalEvent.dataTransfer && e.originalEvent.dataTransfer.files.length) {
+ $dropzone.on('dragenter', function(e) {
+ $(this).addClass('hover');
+ });
+ $dropzone.on('dragleave', function(e) {
$(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();
- });
+ $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]);
+ }
+ });
- this.renderToolbar(fieldModel);
- break;
- }
+ $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]);
+ }
+ });
+ });
- case 'changed':
- break;
+ // 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();
+ });
- case 'saving':
- if (from === 'invalid') {
- this.removeValidationErrors();
+ this.renderToolbar(fieldModel);
+ break;
}
- this.save(options);
- break;
+ case 'changed':
+ break;
- case 'saved':
- break;
+ case 'saving':
+ if (from === 'invalid') {
+ this.removeValidationErrors();
+ }
- case 'invalid':
- this.showValidationErrors();
- break;
- }
- },
+ this.save(options);
+ 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);
- },
- });
- },
+ case 'saved':
+ break;
- /**
- * 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);
+ case 'invalid':
+ this.showValidationErrors();
+ break;
}
- };
-
- $.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'));
+ },
+
+ /**
+ * 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;
- self.ajax({
- type: 'GET',
+ this.ajax({
+ type: 'POST',
url,
+ data,
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();
+ 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);
},
});
- }
- },
-
- /**
- * 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'));
- },
+ },
+
+ /**
+ * 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.'),
+ );
+ },
+ };
- /**
- * @inheritdoc
- */
- getQuickEditUISettings() {
- return { padding: false, unifiedToolbar: true, fullWidthToolbar: true, popup: false };
- },
+ const ajaxOptions = $.extend(defaultOptions, options);
+ const successCallback = ajaxOptions.success;
- /**
- * @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();
- },
+ // 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);
+ }
- /**
- * @inheritdoc
- */
- removeValidationErrors() {
- $(`#${this.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`)
- .find('.quickedit-image-errors').remove();
- this.getEditedElement()
- .removeClass('quickedit-validation-error');
+ 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));
+ );
+})(jQuery, _, Drupal);