--- /dev/null
+/**
+ * @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 has indicated he wants to do 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.
+ var 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 indicated he wants to save his changes 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));