/**
* @file
* Text editor-based in-place editor for formatted text content in Drupal.
*
* Depends on editor.module. Works with any (WYSIWYG) editor that implements the
* editor.js API, including the optional attachInlineEditor() and onChange()
* methods.
* For example, assuming that a hypothetical editor's name was "Magical Editor"
* and its editor.js API implementation lived at Drupal.editors.magical, this
* JavaScript would use:
* - Drupal.editors.magical.attachInlineEditor()
*/
(function ($, Drupal, drupalSettings, _) {
Drupal.quickedit.editors.editor = Drupal.quickedit.EditorView.extend(
/** @lends Drupal.quickedit.editors.editor# */ {
/**
* The text format for this field.
*
* @type {string}
*/
textFormat: null,
/**
* Indicates whether this text format has transformations.
*
* @type {bool}
*/
textFormatHasTransformations: null,
/**
* Stores a reference to the text editor object for this field.
*
* @type {Drupal.quickedit.EditorModel}
*/
textEditor: null,
/**
* Stores the textual DOM element that is being in-place edited.
*
* @type {jQuery}
*/
$textElement: null,
/**
* @constructs
*
* @augments Drupal.quickedit.EditorView
*
* @param {object} options
* Options for the editor view.
*/
initialize(options) {
Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
const metadata = Drupal.quickedit.metadata.get(
this.fieldModel.get('fieldID'),
'custom',
);
this.textFormat = drupalSettings.editor.formats[metadata.format];
this.textFormatHasTransformations = metadata.formatHasTransformations;
this.textEditor = Drupal.editors[this.textFormat.editor];
// Store the actual value of this field. We'll need this to restore the
// original value when the user discards their modifications.
const $fieldItems = this.$el.find('.quickedit-field');
if ($fieldItems.length) {
this.$textElement = $fieldItems.eq(0);
} else {
this.$textElement = this.$el;
}
this.model.set('originalValue', this.$textElement.html());
},
/**
* {@inheritdoc}
*
* @return {jQuery}
* The text element edited.
*/
getEditedElement() {
return this.$textElement;
},
/**
* {@inheritdoc}
*
* @param {object} fieldModel
* The field model.
* @param {string} state
* The current state.
*/
stateChange(fieldModel, state) {
const editorModel = this.model;
const from = fieldModel.previous('state');
const to = state;
switch (to) {
case 'inactive':
break;
case 'candidate':
// Detach the text editor when entering the 'candidate' state from one
// of the states where it could have been attached.
if (from !== 'inactive' && from !== 'highlighted') {
this.textEditor.detach(this.$textElement.get(0), this.textFormat);
}
// A field model's editor view revert() method is invoked when an
// 'active' field becomes a 'candidate' field. But, in the case of
// this in-place editor, the content will have been *replaced* if the
// text format has transformation filters. Therefore, if we stop
// in-place editing this entity, revert explicitly.
if (from === 'active' && this.textFormatHasTransformations) {
this.revert();
}
if (from === 'invalid') {
this.removeValidationErrors();
}
break;
case 'highlighted':
break;
case 'activating':
// When transformation filters have been applied to the formatted text
// of this field, then we'll need to load a re-formatted version of it
// without the transformation filters.
if (this.textFormatHasTransformations) {
const $textElement = this.$textElement;
this._getUntransformedText((untransformedText) => {
$textElement.html(untransformedText);
fieldModel.set('state', 'active');
});
}
// When no transformation filters have been applied: start WYSIWYG
// editing immediately!
else {
// Defer updating the 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 textElement = this.$textElement.get(0);
const toolbarView = fieldModel.toolbarView;
this.textEditor.attachInlineEditor(
textElement,
this.textFormat,
toolbarView.getMainWysiwygToolgroupId(),
toolbarView.getFloatedWysiwygToolgroupId(),
);
// Set the state to 'changed' whenever the content has changed.
this.textEditor.onChange(textElement, (htmlText) => {
editorModel.set('currentValue', htmlText);
fieldModel.set('state', 'changed');
});
break;
}
case 'changed':
break;
case 'saving':
if (from === 'invalid') {
this.removeValidationErrors();
}
this.save();
break;
case 'saved':
break;
case 'invalid':
this.showValidationErrors();
break;
}
},
/**
* {@inheritdoc}
*
* @return {object}
* The settings for the quick edit UI.
*/
getQuickEditUISettings() {
return {
padding: true,
unifiedToolbar: true,
fullWidthToolbar: true,
popup: false,
};
},
/**
* {@inheritdoc}
*/
revert() {
this.$textElement.html(this.model.get('originalValue'));
},
/**
* Loads untransformed text for this field.
*
* More accurately: it re-filters formatted text to exclude transformation
* filters used by the text format.
*
* @param {function} callback
* A callback function that will receive the untransformed text.
*
* @see \Drupal\editor\Ajax\GetUntransformedTextCommand
*/
_getUntransformedText(callback) {
const fieldID = this.fieldModel.get('fieldID');
// Create a Drupal.ajax instance to load the form.
const textLoaderAjax = Drupal.ajax({
url: Drupal.quickedit.util.buildUrl(
fieldID,
Drupal.url(
'editor/!entity_type/!id/!field_name/!langcode/!view_mode',
),
),
submit: { nocssjs: true },
});
// Implement a scoped editorGetUntransformedText AJAX command: calls the
// callback.
textLoaderAjax.commands.editorGetUntransformedText = function (
ajax,
response,
status,
) {
callback(response.data);
};
// This will ensure our scoped editorGetUntransformedText AJAX command
// gets called.
textLoaderAjax.execute();
},
},
);
})(jQuery, Drupal, drupalSettings, _);