/**
* @file
* An abstract Backbone View that controls an in-place editor.
*/
(function ($, Backbone, Drupal) {
Drupal.quickedit.EditorView = Backbone.View.extend(
/** @lends Drupal.quickedit.EditorView# */ {
/**
* A base implementation that outlines the structure for in-place editors.
*
* Specific in-place editor implementations should subclass (extend) this
* View and override whichever method they deem necessary to override.
*
* Typically you would want to override this method to set the
* originalValue attribute in the FieldModel to such a value that your
* in-place editor can revert to the original value when necessary.
*
* @example
* <caption>If you override this method, you should call this
* method (the parent class' initialize()) first.</caption>
* Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
*
* @constructs
*
* @augments Backbone.View
*
* @param {object} options
* An object with the following keys:
* @param {Drupal.quickedit.EditorModel} options.model
* The in-place editor state model.
* @param {Drupal.quickedit.FieldModel} options.fieldModel
* The field model.
*
* @see Drupal.quickedit.EditorModel
* @see Drupal.quickedit.editors.plain_text
*/
initialize(options) {
this.fieldModel = options.fieldModel;
this.listenTo(this.fieldModel, 'change:state', this.stateChange);
},
/**
* {@inheritdoc}
*/
remove() {
// The el property is the field, which should not be removed. Remove the
// pointer to it, then call Backbone.View.prototype.remove().
this.setElement();
Backbone.View.prototype.remove.call(this);
},
/**
* Returns the edited element.
*
* For some single cardinality fields, it may be necessary or useful to
* not in-place edit (and hence decorate) the DOM element with the
* data-quickedit-field-id attribute (which is the field's wrapper), but a
* specific element within the field's wrapper.
* e.g. using a WYSIWYG editor on a body field should happen on the DOM
* element containing the text itself, not on the field wrapper.
*
* @return {jQuery}
* A jQuery-wrapped DOM element.
*
* @see Drupal.quickedit.editors.plain_text
*/
getEditedElement() {
return this.$el;
},
/**
*
* @return {object}
* Returns 3 Quick Edit UI settings that depend on the in-place editor:
* - Boolean padding: indicates whether padding should be applied to the
* edited element, to guarantee legibility of text.
* - Boolean unifiedToolbar: provides the in-place editor with the ability
* to insert its own toolbar UI into Quick Edit's tightly integrated
* toolbar.
* - Boolean fullWidthToolbar: indicates whether Quick Edit's tightly
* integrated toolbar should consume the full width of the element,
* rather than being just long enough to accommodate a label.
*/
getQuickEditUISettings() {
return {
padding: false,
unifiedToolbar: false,
fullWidthToolbar: false,
popup: false,
};
},
/**
* Determines the actions to take given a change of state.
*
* @param {Drupal.quickedit.FieldModel} fieldModel
* The quickedit `FieldModel` that holds the state.
* @param {string} state
* The state of the associated field. One of
* {@link Drupal.quickedit.FieldModel.states}.
*/
stateChange(fieldModel, state) {
const from = fieldModel.previous('state');
const to = state;
switch (to) {
case 'inactive':
// An in-place editor view will not yet exist in this state, hence
// this will never be reached. Listed for sake of completeness.
break;
case 'candidate':
// Nothing to do for the typical in-place editor: it should not be
// visible yet. Except when we come from the 'invalid' state, then we
// clean up.
if (from === 'invalid') {
this.removeValidationErrors();
}
break;
case 'highlighted':
// Nothing to do for the typical in-place editor: it should not be
// visible yet.
break;
case 'activating': {
// The user is in the process of activating in-place editing: if
// something needs to be loaded (CSS/JavaScript/server data/…), then
// do so at this stage, and once the in-place editor is ready,
// set the 'active' state. A "loading" indicator will be shown in the
// UI for as long as the field remains in this state.
const loadDependencies = function (callback) {
// Do the loading here.
callback();
};
loadDependencies(() => {
fieldModel.set('state', 'active');
});
break;
}
case 'active':
// The user can now actually use the in-place editor.
break;
case 'changed':
// Nothing to do for the typical in-place editor. The UI will show an
// indicator that the field has changed.
break;
case 'saving':
// When the user has triggered a save to this field, this state will
// be entered. If the previous saving attempt resulted in validation
// errors, the previous state will be 'invalid'. Clean up those
// validation errors while the user is saving.
if (from === 'invalid') {
this.removeValidationErrors();
}
this.save();
break;
case 'saved':
// Nothing to do for the typical in-place editor. Immediately after
// being saved, a field will go to the 'candidate' state, where it
// should no longer be visible (after all, the field will then again
// just be a *candidate* to be in-place edited).
break;
case 'invalid':
// The modified field value was attempted to be saved, but there were
// validation errors.
this.showValidationErrors();
break;
}
},
/**
* Reverts the modified value to the original, before editing started.
*/
revert() {
// A no-op by default; each editor should implement reverting itself.
// Note that if the in-place editor does not cause the FieldModel's
// element to be modified, then nothing needs to happen.
},
/**
* Saves the modified value in the in-place editor for this field.
*/
save() {
const fieldModel = this.fieldModel;
const editorModel = this.model;
const backstageId = `quickedit_backstage-${this.fieldModel.id.replace(
/[/[\]_\s]/g,
'-',
)}`;
function fillAndSubmitForm(value) {
const $form = $(`#${backstageId}`).find('form');
// Fill in the value in any <input> that isn't hidden or a submit
// button.
$form
.find(':input[type!="hidden"][type!="submit"]:not(select)')
// Don't mess with the node summary.
.not('[name$="\\[summary\\]"]')
.val(value);
// Submit the form.
$form.find('.quickedit-form-submit').trigger('click.quickedit');
}
const formOptions = {
fieldID: this.fieldModel.get('fieldID'),
$el: this.$el,
nocssjs: true,
other_view_modes: fieldModel.findOtherViewModes(),
// Reset an existing entry for this entity in the PrivateTempStore (if
// any) when saving the field. Logically speaking, this should happen in
// a separate request because this is an entity-level operation, not a
// field-level operation. But that would require an additional request,
// that might not even be necessary: it is only when a user saves a
// first changed field for an entity that this needs to happen:
// precisely now!
reset: !this.fieldModel.get('entity').get('inTempStore'),
};
const self = this;
Drupal.quickedit.util.form.load(formOptions, (form, ajax) => {
// Create a backstage area for storing forms that are hidden from view
// (hence "backstage" — since the editing doesn't happen in the form, it
// happens "directly" in the content, the form is only used for saving).
const $backstage = $(
Drupal.theme('quickeditBackstage', { id: backstageId }),
).appendTo('body');
// Hidden forms are stuffed into the backstage container for this field.
const $form = $(form).appendTo($backstage);
// Disable the browser's HTML5 validation; we only care about server-
// side validation. (Not disabling this will actually cause problems
// because browsers don't like to set HTML5 validation errors on hidden
// forms.)
$form.prop('novalidate', true);
const $submit = $form.find('.quickedit-form-submit');
self.formSaveAjax = Drupal.quickedit.util.form.ajaxifySaving(
formOptions,
$submit,
);
function removeHiddenForm() {
Drupal.quickedit.util.form.unajaxifySaving(self.formSaveAjax);
delete self.formSaveAjax;
$backstage.remove();
}
// Successfully saved.
self.formSaveAjax.commands.quickeditFieldFormSaved = function (
ajax,
response,
status,
) {
removeHiddenForm();
// First, transition the state to 'saved'.
fieldModel.set('state', 'saved');
// Second, set the 'htmlForOtherViewModes' attribute, so that when
// this field is rerendered, the change can be propagated to other
// instances of this field, which may be displayed in different view
// modes.
fieldModel.set('htmlForOtherViewModes', response.other_view_modes);
// Finally, set the 'html' attribute on the field model. This will
// cause the field to be rerendered.
fieldModel.set('html', response.data);
};
// Unsuccessfully saved; validation errors.
self.formSaveAjax.commands.quickeditFieldFormValidationErrors = function (
ajax,
response,
status,
) {
removeHiddenForm();
editorModel.set('validationErrors', response.data);
fieldModel.set('state', 'invalid');
};
// The quickeditFieldForm AJAX command is only called upon loading the
// form for the first time, and when there are validation errors in the
// form; Form API then marks which form items have errors. This is
// useful for the form-based in-place editor, but pointless for any
// other: the form itself won't be visible at all anyway! So, we just
// ignore it.
self.formSaveAjax.commands.quickeditFieldForm = function () {};
fillAndSubmitForm(editorModel.get('currentValue'));
});
},
/**
* Shows validation error messages.
*
* Should be called when the state is changed to 'invalid'.
*/
showValidationErrors() {
const $errors = $(
'<div class="quickedit-validation-errors"></div>',
).append(this.model.get('validationErrors'));
this.getEditedElement()
.addClass('quickedit-validation-error')
.after($errors);
},
/**
* Cleans up validation error messages.
*
* Should be called when the state is changed to 'candidate' or 'saving'. In
* the case of the latter: the user has modified the value in the in-place
* editor again to attempt to save again. In the case of the latter: the
* invalid value was discarded.
*/
removeValidationErrors() {
this.getEditedElement()
.removeClass('quickedit-validation-error')
.next('.quickedit-validation-errors')
.remove();
},
},
);
})(jQuery, Backbone, Drupal);