Source: modules/quickedit/js/editors/formEditor.es6.js

/**
 * @file
 * Form-based in-place editor. Works for any field type.
 */

(function ($, Drupal, _) {
  /**
   * @constructor
   *
   * @augments Drupal.quickedit.EditorView
   */
  Drupal.quickedit.editors.form = Drupal.quickedit.EditorView.extend(
    /** @lends Drupal.quickedit.editors.form# */ {
      /**
       * Tracks form container DOM element that is used while in-place editing.
       *
       * @type {jQuery}
       */
      $formContainer: null,

      /**
       * Holds the {@link Drupal.Ajax} object.
       *
       * @type {Drupal.Ajax}
       */
      formSaveAjax: null,

      /**
       * {@inheritdoc}
       *
       * @param {object} fieldModel
       *   The field model that holds the state.
       * @param {string} state
       *   The state to change to.
       */
      stateChange(fieldModel, state) {
        const from = fieldModel.previous('state');
        const to = state;
        switch (to) {
          case 'inactive':
            break;

          case 'candidate':
            if (from !== 'inactive') {
              this.removeForm();
            }
            break;

          case 'highlighted':
            break;

          case 'activating':
            // If coming from an invalid state, then the form is already loaded.
            if (from !== 'invalid') {
              this.loadForm();
            }
            break;

          case 'active':
            break;

          case 'changed':
            break;

          case 'saving':
            this.save();
            break;

          case 'saved':
            break;

          case 'invalid':
            this.showValidationErrors();
            break;
        }
      },

      /**
       * {@inheritdoc}
       *
       * @return {object}
       *   A settings object for the quick edit UI.
       */
      getQuickEditUISettings() {
        return {
          padding: true,
          unifiedToolbar: true,
          fullWidthToolbar: true,
          popup: true,
        };
      },

      /**
       * Loads the form for this field, displays it on top of the actual field.
       */
      loadForm() {
        const fieldModel = this.fieldModel;

        // Generate a DOM-compatible ID for the form container DOM element.
        const id = `quickedit-form-for-${fieldModel.id.replace(
          /[/[\]]/g,
          '_',
        )}`;

        // Render form container.
        const $formContainer = $(
          Drupal.theme('quickeditFormContainer', {
            id,
            loadingMsg: Drupal.t('Loading…'),
          }),
        );
        this.$formContainer = $formContainer;
        $formContainer
          .find('.quickedit-form')
          .addClass(
            'quickedit-editable quickedit-highlighted quickedit-editing',
          )
          .attr('role', 'dialog');

        // Insert form container in DOM.
        if (this.$el.css('display') === 'inline') {
          $formContainer.prependTo(this.$el.offsetParent());
          // Position the form container to render on top of the field's element.
          const pos = this.$el.position();
          $formContainer.css('left', pos.left).css('top', pos.top);
        } else {
          $formContainer.insertBefore(this.$el);
        }

        // Load form, insert it into the form container and attach event handlers.
        const formOptions = {
          fieldID: fieldModel.get('fieldID'),
          $el: this.$el,
          nocssjs: false,
          // Reset an existing entry for this entity in the PrivateTempStore (if
          // any) when loading 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
          // loads a first changed field for an entity that this needs to happen:
          // precisely now!
          reset: !fieldModel.get('entity').get('inTempStore'),
        };
        Drupal.quickedit.util.form.load(formOptions, (form, ajax) => {
          Drupal.AjaxCommands.prototype.insert(ajax, {
            data: form,
            selector: `#${id} .placeholder`,
          });

          $formContainer
            .on('formUpdated.quickedit', ':input', (event) => {
              const state = fieldModel.get('state');
              // If the form is in an invalid state, it will persist on the page.
              // Set the field to activating so that the user can correct the
              // invalid value.
              if (state === 'invalid') {
                fieldModel.set('state', 'activating');
              }
              // Otherwise assume that the fieldModel is in a candidate state and
              // set it to changed on formUpdate.
              else {
                fieldModel.set('state', 'changed');
              }
            })
            .on('keypress.quickedit', 'input', (event) => {
              if (event.keyCode === 13) {
                return false;
              }
            });

          // The in-place editor has loaded; change state to 'active'.
          fieldModel.set('state', 'active');
        });
      },

      /**
       * Removes the form for this field, detaches behaviors and event handlers.
       */
      removeForm() {
        if (this.$formContainer === null) {
          return;
        }

        delete this.formSaveAjax;
        // Allow form widgets to detach properly.
        Drupal.detachBehaviors(this.$formContainer.get(0), null, 'unload');
        this.$formContainer
          .off('change.quickedit', ':input')
          .off('keypress.quickedit', 'input')
          .remove();
        this.$formContainer = null;
      },

      /**
       * {@inheritdoc}
       */
      save() {
        const $formContainer = this.$formContainer;
        const $submit = $formContainer.find('.quickedit-form-submit');
        const editorModel = this.model;
        const fieldModel = this.fieldModel;

        // Create an AJAX object for the form associated with the field.
        let formSaveAjax = Drupal.quickedit.util.form.ajaxifySaving(
          {
            nocssjs: false,
            other_view_modes: fieldModel.findOtherViewModes(),
          },
          $submit,
        );

        function cleanUpAjax() {
          Drupal.quickedit.util.form.unajaxifySaving(formSaveAjax);
          formSaveAjax = null;
        }

        // Successfully saved.
        formSaveAjax.commands.quickeditFieldFormSaved = function (
          ajax,
          response,
          status,
        ) {
          cleanUpAjax();
          // 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.
          _.defer(() => {
            fieldModel.set('html', response.data);
          });
        };

        // Unsuccessfully saved; validation errors.
        formSaveAjax.commands.quickeditFieldFormValidationErrors = function (
          ajax,
          response,
          status,
        ) {
          editorModel.set('validationErrors', response.data);
          fieldModel.set('state', 'invalid');
        };

        // The quickeditFieldForm AJAX command is called upon attempting to save
        // the form; Form API will mark which form items have errors, if any. This
        // command is invoked only if validation errors exist and then it runs
        // before editFieldFormValidationErrors().
        formSaveAjax.commands.quickeditFieldForm = function (
          ajax,
          response,
          status,
        ) {
          Drupal.AjaxCommands.prototype.insert(ajax, {
            data: response.data,
            selector: `#${$formContainer.attr('id')} form`,
          });
        };

        // Click the form's submit button; the scoped AJAX commands above will
        // handle the server's response.
        $submit.trigger('click.quickedit');
      },

      /**
       * {@inheritdoc}
       */
      showValidationErrors() {
        this.$formContainer
          .find('.quickedit-form')
          .addClass('quickedit-validation-error')
          .find('form')
          .prepend(this.model.get('validationErrors'));
      },
    },
  );
})(jQuery, Drupal, _);