Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / modules / quickedit / js / models / EntityModel.es6.js
index 4ea83713562dd76d78f97095fbb929c13153dad7..c3f10e95e050757379a3e5d1d568937402909746 100644 (file)
  * A Backbone Model for the state of an in-place editable entity in the DOM.
  */
 
-(function (_, $, Backbone, Drupal) {
-  Drupal.quickedit.EntityModel = Drupal.quickedit.BaseModel.extend(/** @lends Drupal.quickedit.EntityModel# */{
-
-    /**
-     * @type {object}
-     */
-    defaults: /** @lends Drupal.quickedit.EntityModel# */{
-
+(function(_, $, Backbone, Drupal) {
+  Drupal.quickedit.EntityModel = Drupal.quickedit.BaseModel.extend(
+    /** @lends Drupal.quickedit.EntityModel# */ {
       /**
-       * The DOM element that represents this entity.
-       *
-       * It may seem bizarre to have a DOM element in a Backbone Model, but we
-       * need to be able to map entities in the DOM to EntityModels in memory.
-       *
-       * @type {HTMLElement}
+       * @type {object}
        */
-      el: null,
+      defaults: /** @lends Drupal.quickedit.EntityModel# */ {
+        /**
+         * The DOM element that represents this entity.
+         *
+         * It may seem bizarre to have a DOM element in a Backbone Model, but we
+         * need to be able to map entities in the DOM to EntityModels in memory.
+         *
+         * @type {HTMLElement}
+         */
+        el: null,
+
+        /**
+         * An entity ID, of the form `<entity type>/<entity ID>`
+         *
+         * @example
+         * "node/1"
+         *
+         * @type {string}
+         */
+        entityID: null,
+
+        /**
+         * An entity instance ID.
+         *
+         * The first instance of a specific entity (i.e. with a given entity ID)
+         * is assigned 0, the second 1, and so on.
+         *
+         * @type {number}
+         */
+        entityInstanceID: null,
+
+        /**
+         * The unique ID of this entity instance on the page, of the form
+         * `<entity type>/<entity ID>[entity instance ID]`
+         *
+         * @example
+         * "node/1[0]"
+         *
+         * @type {string}
+         */
+        id: null,
+
+        /**
+         * The label of the entity.
+         *
+         * @type {string}
+         */
+        label: null,
+
+        /**
+         * A FieldCollection for all fields of the entity.
+         *
+         * @type {Drupal.quickedit.FieldCollection}
+         *
+         * @see Drupal.quickedit.FieldCollection
+         */
+        fields: null,
+
+        // The attributes below are stateful. The ones above will never change
+        // during the life of a EntityModel instance.
+
+        /**
+         * Indicates whether this entity is currently being edited in-place.
+         *
+         * @type {bool}
+         */
+        isActive: false,
+
+        /**
+         * Whether one or more fields are already been stored in PrivateTempStore.
+         *
+         * @type {bool}
+         */
+        inTempStore: false,
+
+        /**
+         * Indicates whether a "Save" button is necessary or not.
+         *
+         * Whether one or more fields have already been stored in PrivateTempStore
+         * *or* the field that's currently being edited is in the 'changed' or a
+         * later state.
+         *
+         * @type {bool}
+         */
+        isDirty: false,
+
+        /**
+         * Whether the request to the server has been made to commit this entity.
+         *
+         * Used to prevent multiple such requests.
+         *
+         * @type {bool}
+         */
+        isCommitting: false,
+
+        /**
+         * The current processing state of an entity.
+         *
+         * @type {string}
+         */
+        state: 'closed',
+
+        /**
+         * IDs of fields whose new values have been stored in PrivateTempStore.
+         *
+         * We must store this on the EntityModel as well (even though it already
+         * is on the FieldModel) because when a field is rerendered, its
+         * FieldModel is destroyed and this allows us to transition it back to
+         * the proper state.
+         *
+         * @type {Array.<string>}
+         */
+        fieldsInTempStore: [],
+
+        /**
+         * A flag the tells the application that this EntityModel must be reloaded
+         * in order to restore the original values to its fields in the client.
+         *
+         * @type {bool}
+         */
+        reload: false,
+      },
 
       /**
-       * An entity ID, of the form `<entity type>/<entity ID>`
-       *
-       * @example
-       * "node/1"
+       * @constructs
        *
-       * @type {string}
+       * @augments Drupal.quickedit.BaseModel
        */
-      entityID: null,
+      initialize() {
+        this.set('fields', new Drupal.quickedit.FieldCollection());
 
-      /**
-       * An entity instance ID.
-       *
-       * The first instance of a specific entity (i.e. with a given entity ID)
-       * is assigned 0, the second 1, and so on.
-       *
-       * @type {number}
-       */
-      entityInstanceID: null,
+        // Respond to entity state changes.
+        this.listenTo(this, 'change:state', this.stateChange);
 
-      /**
-       * The unique ID of this entity instance on the page, of the form
-       * `<entity type>/<entity ID>[entity instance ID]`
-       *
-       * @example
-       * "node/1[0]"
-       *
-       * @type {string}
-       */
-      id: null,
+        // The state of the entity is largely dependent on the state of its
+        // fields.
+        this.listenTo(
+          this.get('fields'),
+          'change:state',
+          this.fieldStateChange,
+        );
 
-      /**
-       * The label of the entity.
-       *
-       * @type {string}
-       */
-      label: null,
+        // Call Drupal.quickedit.BaseModel's initialize() method.
+        Drupal.quickedit.BaseModel.prototype.initialize.call(this);
+      },
 
       /**
-       * A FieldCollection for all fields of the entity.
-       *
-       * @type {Drupal.quickedit.FieldCollection}
+       * Updates FieldModels' states when an EntityModel change occurs.
        *
-       * @see Drupal.quickedit.FieldCollection
+       * @param {Drupal.quickedit.EntityModel} entityModel
+       *   The entity model
+       * @param {string} state
+       *   The state of the associated entity. One of
+       *   {@link Drupal.quickedit.EntityModel.states}.
+       * @param {object} options
+       *   Options for the entity model.
        */
-      fields: null,
-
-      // The attributes below are stateful. The ones above will never change
-      // during the life of a EntityModel instance.
+      stateChange(entityModel, state, options) {
+        const to = state;
+        switch (to) {
+          case 'closed':
+            this.set({
+              isActive: false,
+              inTempStore: false,
+              isDirty: false,
+            });
+            break;
 
-      /**
-       * Indicates whether this entity is currently being edited in-place.
-       *
-       * @type {bool}
-       */
-      isActive: false,
+          case 'launching':
+            break;
 
-      /**
-       * Whether one or more fields are already been stored in PrivateTempStore.
-       *
-       * @type {bool}
-       */
-      inTempStore: false,
+          case 'opening':
+            // Set the fields to candidate state.
+            entityModel.get('fields').each(fieldModel => {
+              fieldModel.set('state', 'candidate', options);
+            });
+            break;
+
+          case 'opened':
+            // The entity is now ready for editing!
+            this.set('isActive', true);
+            break;
+
+          case 'committing': {
+            // The user indicated they want to save the entity.
+            const fields = this.get('fields');
+            // For fields that are in an active state, transition them to
+            // candidate.
+            fields
+              .chain()
+              .filter(
+                fieldModel =>
+                  _.intersection([fieldModel.get('state')], ['active']).length,
+              )
+              .each(fieldModel => {
+                fieldModel.set('state', 'candidate');
+              });
+            // For fields that are in a changed state, field values must first be
+            // stored in PrivateTempStore.
+            fields
+              .chain()
+              .filter(
+                fieldModel =>
+                  _.intersection(
+                    [fieldModel.get('state')],
+                    Drupal.quickedit.app.changedFieldStates,
+                  ).length,
+              )
+              .each(fieldModel => {
+                fieldModel.set('state', 'saving');
+              });
+            break;
+          }
 
-      /**
-       * Indicates whether a "Save" button is necessary or not.
-       *
-       * Whether one or more fields have already been stored in PrivateTempStore
-       * *or* the field that's currently being edited is in the 'changed' or a
-       * later state.
-       *
-       * @type {bool}
-       */
-      isDirty: false,
+          case 'deactivating': {
+            const changedFields = this.get('fields').filter(
+              fieldModel =>
+                _.intersection(
+                  [fieldModel.get('state')],
+                  ['changed', 'invalid'],
+                ).length,
+            );
+            // If the entity contains unconfirmed or unsaved changes, return the
+            // entity to an opened state and ask the user if they would like to
+            // save the changes or discard the changes.
+            //   1. One of the fields is in a changed state. The changed field
+            //   might just be a change in the client or it might have been saved
+            //   to tempstore.
+            //   2. The saved flag is empty and the confirmed flag is empty. If
+            //   the entity has been saved to the server, the fields changed in
+            //   the client are irrelevant. If the changes are confirmed, then
+            //   proceed to set the fields to candidate state.
+            if (
+              (changedFields.length || this.get('fieldsInTempStore').length) &&
+              (!options.saved && !options.confirmed)
+            ) {
+              // Cancel deactivation until the user confirms save or discard.
+              this.set('state', 'opened', { confirming: true });
+              // An action in reaction to state change must be deferred.
+              _.defer(() => {
+                Drupal.quickedit.app.confirmEntityDeactivation(entityModel);
+              });
+            } else {
+              const invalidFields = this.get('fields').filter(
+                fieldModel =>
+                  _.intersection([fieldModel.get('state')], ['invalid']).length,
+              );
+              // Indicate if this EntityModel needs to be reloaded in order to
+              // restore the original values of its fields.
+              entityModel.set(
+                'reload',
+                this.get('fieldsInTempStore').length || invalidFields.length,
+              );
+              // Set all fields to the 'candidate' state. A changed field may have
+              // to go through confirmation first.
+              entityModel.get('fields').each(fieldModel => {
+                // If the field is already in the candidate state, trigger a
+                // change event so that the entityModel can move to the next state
+                // in deactivation.
+                if (
+                  _.intersection(
+                    [fieldModel.get('state')],
+                    ['candidate', 'highlighted'],
+                  ).length
+                ) {
+                  fieldModel.trigger(
+                    'change:state',
+                    fieldModel,
+                    fieldModel.get('state'),
+                    options,
+                  );
+                } else {
+                  fieldModel.set('state', 'candidate', options);
+                }
+              });
+            }
+            break;
+          }
 
-      /**
-       * Whether the request to the server has been made to commit this entity.
-       *
-       * Used to prevent multiple such requests.
-       *
-       * @type {bool}
-       */
-      isCommitting: false,
+          case 'closing':
+            // Set all fields to the 'inactive' state.
+            options.reason = 'stop';
+            this.get('fields').each(fieldModel => {
+              fieldModel.set(
+                {
+                  inTempStore: false,
+                  state: 'inactive',
+                },
+                options,
+              );
+            });
+            break;
+        }
+      },
 
       /**
-       * The current processing state of an entity.
+       * Updates a Field and Entity model's "inTempStore" when appropriate.
        *
-       * @type {string}
-       */
-      state: 'closed',
-
-      /**
-       * IDs of fields whose new values have been stored in PrivateTempStore.
+       * Helper function.
        *
-       * We must store this on the EntityModel as well (even though it already
-       * is on the FieldModel) because when a field is rerendered, its
-       * FieldModel is destroyed and this allows us to transition it back to
-       * the proper state.
+       * @param {Drupal.quickedit.EntityModel} entityModel
+       *   The model of the entity for which a field's state attribute has
+       *   changed.
+       * @param {Drupal.quickedit.FieldModel} fieldModel
+       *   The model of the field whose state attribute has changed.
        *
-       * @type {Array.<string>}
+       * @see Drupal.quickedit.EntityModel#fieldStateChange
        */
-      fieldsInTempStore: [],
+      _updateInTempStoreAttributes(entityModel, fieldModel) {
+        const current = fieldModel.get('state');
+        const previous = fieldModel.previous('state');
+        let fieldsInTempStore = entityModel.get('fieldsInTempStore');
+        // If the fieldModel changed to the 'saved' state: remember that this
+        // field was saved to PrivateTempStore.
+        if (current === 'saved') {
+          // Mark the entity as saved in PrivateTempStore, so that we can pass the
+          // proper "reset PrivateTempStore" boolean value when communicating with
+          // the server.
+          entityModel.set('inTempStore', true);
+          // Mark the field as saved in PrivateTempStore, so that visual
+          // indicators signifying just that may be rendered.
+          fieldModel.set('inTempStore', true);
+          // Remember that this field is in PrivateTempStore, restore when
+          // rerendered.
+          fieldsInTempStore.push(fieldModel.get('fieldID'));
+          fieldsInTempStore = _.uniq(fieldsInTempStore);
+          entityModel.set('fieldsInTempStore', fieldsInTempStore);
+        }
+        // If the fieldModel changed to the 'candidate' state from the
+        // 'inactive' state, then this is a field for this entity that got
+        // rerendered. Restore its previous 'inTempStore' attribute value.
+        else if (current === 'candidate' && previous === 'inactive') {
+          fieldModel.set(
+            'inTempStore',
+            _.intersection([fieldModel.get('fieldID')], fieldsInTempStore)
+              .length > 0,
+          );
+        }
+      },
 
       /**
-       * A flag the tells the application that this EntityModel must be reloaded
-       * in order to restore the original values to its fields in the client.
+       * Reacts to state changes in this entity's fields.
        *
-       * @type {bool}
+       * @param {Drupal.quickedit.FieldModel} fieldModel
+       *   The model of the field whose state attribute changed.
+       * @param {string} state
+       *   The state of the associated field. One of
+       *   {@link Drupal.quickedit.FieldModel.states}.
        */
-      reload: false,
-    },
-
-    /**
-     * @constructs
-     *
-     * @augments Drupal.quickedit.BaseModel
-     */
-    initialize() {
-      this.set('fields', new Drupal.quickedit.FieldCollection());
-
-      // Respond to entity state changes.
-      this.listenTo(this, 'change:state', this.stateChange);
-
-      // The state of the entity is largely dependent on the state of its
-      // fields.
-      this.listenTo(this.get('fields'), 'change:state', this.fieldStateChange);
-
-      // Call Drupal.quickedit.BaseModel's initialize() method.
-      Drupal.quickedit.BaseModel.prototype.initialize.call(this);
-    },
-
-    /**
-     * Updates FieldModels' states when an EntityModel change occurs.
-     *
-     * @param {Drupal.quickedit.EntityModel} entityModel
-     *   The entity model
-     * @param {string} state
-     *   The state of the associated entity. One of
-     *   {@link Drupal.quickedit.EntityModel.states}.
-     * @param {object} options
-     *   Options for the entity model.
-     */
-    stateChange(entityModel, state, options) {
-      const to = state;
-      switch (to) {
-        case 'closed':
-          this.set({
-            isActive: false,
-            inTempStore: false,
-            isDirty: false,
-          });
-          break;
-
-        case 'launching':
-          break;
-
-        case 'opening':
-          // Set the fields to candidate state.
-          entityModel.get('fields').each((fieldModel) => {
-            fieldModel.set('state', 'candidate', options);
-          });
-          break;
-
-        case 'opened':
-          // The entity is now ready for editing!
-          this.set('isActive', true);
-          break;
-
-        case 'committing': {
-          // The user indicated they want to save the entity.
-          const fields = this.get('fields');
-          // For fields that are in an active state, transition them to
-          // candidate.
-          fields.chain()
-            .filter(fieldModel => _.intersection([fieldModel.get('state')], ['active']).length)
-            .each((fieldModel) => {
-              fieldModel.set('state', 'candidate');
-            });
-          // For fields that are in a changed state, field values must first be
-          // stored in PrivateTempStore.
-          fields.chain()
-            .filter(fieldModel => _.intersection([fieldModel.get('state')], Drupal.quickedit.app.changedFieldStates).length)
-            .each((fieldModel) => {
-              fieldModel.set('state', 'saving');
-            });
-          break;
-        }
-
-        case 'deactivating': {
-          const changedFields = this.get('fields')
-            .filter(fieldModel => _.intersection([fieldModel.get('state')], ['changed', 'invalid']).length);
-          // If the entity contains unconfirmed or unsaved changes, return the
-          // entity to an opened state and ask the user if they would like to
-          // save the changes or discard the changes.
-          //   1. One of the fields is in a changed state. The changed field
-          //   might just be a change in the client or it might have been saved
-          //   to tempstore.
-          //   2. The saved flag is empty and the confirmed flag is empty. If
-          //   the entity has been saved to the server, the fields changed in
-          //   the client are irrelevant. If the changes are confirmed, then
-          //   proceed to set the fields to candidate state.
-          if ((changedFields.length || this.get('fieldsInTempStore').length) && (!options.saved && !options.confirmed)) {
-            // Cancel deactivation until the user confirms save or discard.
-            this.set('state', 'opened', { confirming: true });
-            // An action in reaction to state change must be deferred.
+      fieldStateChange(fieldModel, state) {
+        const entityModel = this;
+        const fieldState = state;
+        // Switch on the entityModel state.
+        // The EntityModel responds to FieldModel state changes as a function of
+        // its state. For example, a field switching back to 'candidate' state
+        // when its entity is in the 'opened' state has no effect on the entity.
+        // But that same switch back to 'candidate' state of a field when the
+        // entity is in the 'committing' state might allow the entity to proceed
+        // with the commit flow.
+        switch (this.get('state')) {
+          case 'closed':
+          case 'launching':
+            // It should be impossible to reach these: fields can't change state
+            // while the entity is closed or still launching.
+            break;
+
+          case 'opening':
+            // We must change the entity to the 'opened' state, but it must first
+            // be confirmed that all of its fieldModels have transitioned to the
+            // 'candidate' state.
+            // We do this here, because this is called every time a fieldModel
+            // changes state, hence each time this is called, we get closer to the
+            // goal of having all fieldModels in the 'candidate' state.
+            // A state change in reaction to another state change must be
+            // deferred.
             _.defer(() => {
-              Drupal.quickedit.app.confirmEntityDeactivation(entityModel);
+              entityModel.set('state', 'opened', {
+                'accept-field-states': Drupal.quickedit.app.readyFieldStates,
+              });
             });
-          }
-          else {
-            const invalidFields = this.get('fields')
-              .filter(fieldModel => _.intersection([fieldModel.get('state')], ['invalid']).length);
-            // Indicate if this EntityModel needs to be reloaded in order to
-            // restore the original values of its fields.
-            entityModel.set('reload', (this.get('fieldsInTempStore').length || invalidFields.length));
-            // Set all fields to the 'candidate' state. A changed field may have
-            // to go through confirmation first.
-            entityModel.get('fields').each((fieldModel) => {
-              // If the field is already in the candidate state, trigger a
-              // change event so that the entityModel can move to the next state
-              // in deactivation.
-              if (_.intersection([fieldModel.get('state')], ['candidate', 'highlighted']).length) {
-                fieldModel.trigger('change:state', fieldModel, fieldModel.get('state'), options);
-              }
-              else {
-                fieldModel.set('state', 'candidate', options);
-              }
-            });
-          }
-          break;
-        }
-
-        case 'closing':
-          // Set all fields to the 'inactive' state.
-          options.reason = 'stop';
-          this.get('fields').each((fieldModel) => {
-            fieldModel.set({
-              inTempStore: false,
-              state: 'inactive',
-            }, options);
-          });
-          break;
-      }
-    },
-
-    /**
-     * Updates a Field and Entity model's "inTempStore" when appropriate.
-     *
-     * Helper function.
-     *
-     * @param {Drupal.quickedit.EntityModel} entityModel
-     *   The model of the entity for which a field's state attribute has
-     *   changed.
-     * @param {Drupal.quickedit.FieldModel} fieldModel
-     *   The model of the field whose state attribute has changed.
-     *
-     * @see Drupal.quickedit.EntityModel#fieldStateChange
-     */
-    _updateInTempStoreAttributes(entityModel, fieldModel) {
-      const current = fieldModel.get('state');
-      const previous = fieldModel.previous('state');
-      let fieldsInTempStore = entityModel.get('fieldsInTempStore');
-      // If the fieldModel changed to the 'saved' state: remember that this
-      // field was saved to PrivateTempStore.
-      if (current === 'saved') {
-        // Mark the entity as saved in PrivateTempStore, so that we can pass the
-        // proper "reset PrivateTempStore" boolean value when communicating with
-        // the server.
-        entityModel.set('inTempStore', true);
-        // Mark the field as saved in PrivateTempStore, so that visual
-        // indicators signifying just that may be rendered.
-        fieldModel.set('inTempStore', true);
-        // Remember that this field is in PrivateTempStore, restore when
-        // rerendered.
-        fieldsInTempStore.push(fieldModel.get('fieldID'));
-        fieldsInTempStore = _.uniq(fieldsInTempStore);
-        entityModel.set('fieldsInTempStore', fieldsInTempStore);
-      }
-      // If the fieldModel changed to the 'candidate' state from the
-      // 'inactive' state, then this is a field for this entity that got
-      // rerendered. Restore its previous 'inTempStore' attribute value.
-      else if (current === 'candidate' && previous === 'inactive') {
-        fieldModel.set('inTempStore', _.intersection([fieldModel.get('fieldID')], fieldsInTempStore).length > 0);
-      }
-    },
-
-    /**
-     * Reacts to state changes in this entity's fields.
-     *
-     * @param {Drupal.quickedit.FieldModel} fieldModel
-     *   The model of the field whose state attribute changed.
-     * @param {string} state
-     *   The state of the associated field. One of
-     *   {@link Drupal.quickedit.FieldModel.states}.
-     */
-    fieldStateChange(fieldModel, state) {
-      const entityModel = this;
-      const fieldState = state;
-      // Switch on the entityModel state.
-      // The EntityModel responds to FieldModel state changes as a function of
-      // its state. For example, a field switching back to 'candidate' state
-      // when its entity is in the 'opened' state has no effect on the entity.
-      // But that same switch back to 'candidate' state of a field when the
-      // entity is in the 'committing' state might allow the entity to proceed
-      // with the commit flow.
-      switch (this.get('state')) {
-        case 'closed':
-        case 'launching':
-          // It should be impossible to reach these: fields can't change state
-          // while the entity is closed or still launching.
-          break;
-
-        case 'opening':
-          // We must change the entity to the 'opened' state, but it must first
-          // be confirmed that all of its fieldModels have transitioned to the
-          // 'candidate' state.
-          // We do this here, because this is called every time a fieldModel
-          // changes state, hence each time this is called, we get closer to the
-          // goal of having all fieldModels in the 'candidate' state.
-          // A state change in reaction to another state change must be
-          // deferred.
-          _.defer(() => {
-            entityModel.set('state', 'opened', {
+            break;
+
+          case 'opened':
+            // Set the isDirty attribute when appropriate so that it is known when
+            // to display the "Save" button in the entity toolbar.
+            // Note that once a field has been changed, there's no way to discard
+            // that change, hence it will have to be saved into PrivateTempStore,
+            // or the in-place editing of this field will have to be stopped
+            // completely. In other words: once any field enters the 'changed'
+            // field, then for the remainder of the in-place editing session, the
+            // entity is by definition dirty.
+            if (fieldState === 'changed') {
+              entityModel.set('isDirty', true);
+            } else {
+              this._updateInTempStoreAttributes(entityModel, fieldModel);
+            }
+            break;
+
+          case 'committing': {
+            // If the field save returned a validation error, set the state of the
+            // entity back to 'opened'.
+            if (fieldState === 'invalid') {
+              // A state change in reaction to another state change must be
+              // deferred.
+              _.defer(() => {
+                entityModel.set('state', 'opened', { reason: 'invalid' });
+              });
+            } else {
+              this._updateInTempStoreAttributes(entityModel, fieldModel);
+            }
+
+            // Attempt to save the entity. If the entity's fields are not yet all
+            // in a ready state, the save will not be processed.
+            const options = {
               'accept-field-states': Drupal.quickedit.app.readyFieldStates,
-            });
-          });
-          break;
-
-        case 'opened':
-          // Set the isDirty attribute when appropriate so that it is known when
-          // to display the "Save" button in the entity toolbar.
-          // Note that once a field has been changed, there's no way to discard
-          // that change, hence it will have to be saved into PrivateTempStore,
-          // or the in-place editing of this field will have to be stopped
-          // completely. In other words: once any field enters the 'changed'
-          // field, then for the remainder of the in-place editing session, the
-          // entity is by definition dirty.
-          if (fieldState === 'changed') {
-            entityModel.set('isDirty', true);
+            };
+            if (entityModel.set('isCommitting', true, options)) {
+              entityModel.save({
+                success() {
+                  entityModel.set(
+                    {
+                      state: 'deactivating',
+                      isCommitting: false,
+                    },
+                    { saved: true },
+                  );
+                },
+                error() {
+                  // Reset the "isCommitting" mutex.
+                  entityModel.set('isCommitting', false);
+                  // Change the state back to "opened", to allow the user to hit
+                  // the "Save" button again.
+                  entityModel.set('state', 'opened', {
+                    reason: 'networkerror',
+                  });
+                  // Show a modal to inform the user of the network error.
+                  const message = Drupal.t(
+                    'Your changes to <q>@entity-title</q> could not be saved, either due to a website problem or a network connection problem.<br>Please try again.',
+                    { '@entity-title': entityModel.get('label') },
+                  );
+                  Drupal.quickedit.util.networkErrorModal(
+                    Drupal.t('Network problem!'),
+                    message,
+                  );
+                },
+              });
+            }
+            break;
           }
-          else {
-            this._updateInTempStoreAttributes(entityModel, fieldModel);
-          }
-          break;
 
-        case 'committing': {
-          // If the field save returned a validation error, set the state of the
-          // entity back to 'opened'.
-          if (fieldState === 'invalid') {
+          case 'deactivating':
+            // When setting the entity to 'closing', require that all fieldModels
+            // are in either the 'candidate' or 'highlighted' state.
             // A state change in reaction to another state change must be
             // deferred.
             _.defer(() => {
-              entityModel.set('state', 'opened', { reason: 'invalid' });
+              entityModel.set('state', 'closing', {
+                'accept-field-states': Drupal.quickedit.app.readyFieldStates,
+              });
             });
-          }
-          else {
-            this._updateInTempStoreAttributes(entityModel, fieldModel);
-          }
+            break;
 
-          // Attempt to save the entity. If the entity's fields are not yet all
-          // in a ready state, the save will not be processed.
-          const options = {
-            'accept-field-states': Drupal.quickedit.app.readyFieldStates,
-          };
-          if (entityModel.set('isCommitting', true, options)) {
-            entityModel.save({
-              success() {
-                entityModel.set({
-                  state: 'deactivating',
-                  isCommitting: false,
-                }, { saved: true });
-              },
-              error() {
-                // Reset the "isCommitting" mutex.
-                entityModel.set('isCommitting', false);
-                // Change the state back to "opened", to allow the user to hit
-                // the "Save" button again.
-                entityModel.set('state', 'opened', { reason: 'networkerror' });
-                // Show a modal to inform the user of the network error.
-                const message = Drupal.t('Your changes to <q>@entity-title</q> could not be saved, either due to a website problem or a network connection problem.<br>Please try again.', { '@entity-title': entityModel.get('label') });
-                Drupal.quickedit.util.networkErrorModal(Drupal.t('Network problem!'), message);
-              },
+          case 'closing':
+            // When setting the entity to 'closed', require that all fieldModels
+            // are in the 'inactive' state.
+            // A state change in reaction to another state change must be
+            // deferred.
+            _.defer(() => {
+              entityModel.set('state', 'closed', {
+                'accept-field-states': ['inactive'],
+              });
             });
-          }
-          break;
+            break;
         }
+      },
 
-        case 'deactivating':
-          // When setting the entity to 'closing', require that all fieldModels
-          // are in either the 'candidate' or 'highlighted' state.
-          // A state change in reaction to another state change must be
-          // deferred.
-          _.defer(() => {
-            entityModel.set('state', 'closing', {
-              'accept-field-states': Drupal.quickedit.app.readyFieldStates,
-            });
-          });
-          break;
-
-        case 'closing':
-          // When setting the entity to 'closed', require that all fieldModels
-          // are in the 'inactive' state.
-          // A state change in reaction to another state change must be
-          // deferred.
-          _.defer(() => {
-            entityModel.set('state', 'closed', {
-              'accept-field-states': ['inactive'],
-            });
-          });
-          break;
-      }
-    },
-
-    /**
-     * Fires an AJAX request to the REST save URL for an entity.
-     *
-     * @param {object} options
-     *   An object of options that contains:
-     * @param {function} [options.success]
-     *   A function to invoke if the entity is successfully saved.
-     */
-    save(options) {
-      const entityModel = this;
-
-      // Create a Drupal.ajax instance to save the entity.
-      const entitySaverAjax = Drupal.ajax({
-        url: Drupal.url(`quickedit/entity/${entityModel.get('entityID')}`),
-        error() {
-          // Let the Drupal.quickedit.EntityModel Backbone model's error()
-          // method handle errors.
-          options.error.call(entityModel);
-        },
-      });
-      // Entity saved successfully.
-      entitySaverAjax.commands.quickeditEntitySaved = function (ajax, response, status) {
-        // All fields have been moved from PrivateTempStore to permanent
-        // storage, update the "inTempStore" attribute on FieldModels, on the
-        // EntityModel and clear EntityModel's "fieldInTempStore" attribute.
-        entityModel.get('fields').each((fieldModel) => {
-          fieldModel.set('inTempStore', false);
+      /**
+       * Fires an AJAX request to the REST save URL for an entity.
+       *
+       * @param {object} options
+       *   An object of options that contains:
+       * @param {function} [options.success]
+       *   A function to invoke if the entity is successfully saved.
+       */
+      save(options) {
+        const entityModel = this;
+
+        // Create a Drupal.ajax instance to save the entity.
+        const entitySaverAjax = Drupal.ajax({
+          url: Drupal.url(`quickedit/entity/${entityModel.get('entityID')}`),
+          error() {
+            // Let the Drupal.quickedit.EntityModel Backbone model's error()
+            // method handle errors.
+            options.error.call(entityModel);
+          },
         });
-        entityModel.set('inTempStore', false);
-        entityModel.set('fieldsInTempStore', []);
+        // Entity saved successfully.
+        entitySaverAjax.commands.quickeditEntitySaved = function(
+          ajax,
+          response,
+          status,
+        ) {
+          // All fields have been moved from PrivateTempStore to permanent
+          // storage, update the "inTempStore" attribute on FieldModels, on the
+          // EntityModel and clear EntityModel's "fieldInTempStore" attribute.
+          entityModel.get('fields').each(fieldModel => {
+            fieldModel.set('inTempStore', false);
+          });
+          entityModel.set('inTempStore', false);
+          entityModel.set('fieldsInTempStore', []);
 
-        // Invoke the optional success callback.
-        if (options.success) {
-          options.success.call(entityModel);
-        }
-      };
-      // Trigger the AJAX request, which will will return the
-      // quickeditEntitySaved AJAX command to which we then react.
-      entitySaverAjax.execute();
-    },
+          // Invoke the optional success callback.
+          if (options.success) {
+            options.success.call(entityModel);
+          }
+        };
+        // Trigger the AJAX request, which will will return the
+        // quickeditEntitySaved AJAX command to which we then react.
+        entitySaverAjax.execute();
+      },
 
-    /**
-     * Validate the entity model.
-     *
-     * @param {object} attrs
-     *   The attributes changes in the save or set call.
-     * @param {object} options
-     *   An object with the following option:
-     * @param {string} [options.reason]
-     *   A string that conveys a particular reason to allow for an exceptional
-     *   state change.
-     * @param {Array} options.accept-field-states
-     *   An array of strings that represent field states that the entities must
-     *   be in to validate. For example, if `accept-field-states` is
-     *   `['candidate', 'highlighted']`, then all the fields of the entity must
-     *   be in either of these two states for the save or set call to
-     *   validate and proceed.
-     *
-     * @return {string}
-     *   A string to say something about the state of the entity model.
-     */
-    validate(attrs, options) {
-      const acceptedFieldStates = options['accept-field-states'] || [];
-
-      // Validate state change.
-      const currentState = this.get('state');
-      const nextState = attrs.state;
-      if (currentState !== nextState) {
-        // Ensure it's a valid state.
-        if (_.indexOf(this.constructor.states, nextState) === -1) {
-          return `"${nextState}" is an invalid state`;
-        }
+      /**
+       * Validate the entity model.
+       *
+       * @param {object} attrs
+       *   The attributes changes in the save or set call.
+       * @param {object} options
+       *   An object with the following option:
+       * @param {string} [options.reason]
+       *   A string that conveys a particular reason to allow for an exceptional
+       *   state change.
+       * @param {Array} options.accept-field-states
+       *   An array of strings that represent field states that the entities must
+       *   be in to validate. For example, if `accept-field-states` is
+       *   `['candidate', 'highlighted']`, then all the fields of the entity must
+       *   be in either of these two states for the save or set call to
+       *   validate and proceed.
+       *
+       * @return {string}
+       *   A string to say something about the state of the entity model.
+       */
+      validate(attrs, options) {
+        const acceptedFieldStates = options['accept-field-states'] || [];
+
+        // Validate state change.
+        const currentState = this.get('state');
+        const nextState = attrs.state;
+        if (currentState !== nextState) {
+          // Ensure it's a valid state.
+          if (_.indexOf(this.constructor.states, nextState) === -1) {
+            return `"${nextState}" is an invalid state`;
+          }
 
-        // Ensure it's a state change that is allowed.
-        // Check if the acceptStateChange function accepts it.
-        if (!this._acceptStateChange(currentState, nextState, options)) {
-          return 'state change not accepted';
-        }
-        // If that function accepts it, then ensure all fields are also in an
-        // acceptable state.
-        else if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) {
-          return 'state change not accepted because fields are not in acceptable state';
-        }
-      }
-
-      // Validate setting isCommitting = true.
-      const currentIsCommitting = this.get('isCommitting');
-      const nextIsCommitting = attrs.isCommitting;
-      if (currentIsCommitting === false && nextIsCommitting === true) {
-        if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) {
-          return 'isCommitting change not accepted because fields are not in acceptable state';
+          // Ensure it's a state change that is allowed.
+          // Check if the acceptStateChange function accepts it.
+          if (!this._acceptStateChange(currentState, nextState, options)) {
+            return 'state change not accepted';
+          }
+          // If that function accepts it, then ensure all fields are also in an
+          // acceptable state.
+          if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) {
+            return 'state change not accepted because fields are not in acceptable state';
+          }
         }
-      }
-      else if (currentIsCommitting === true && nextIsCommitting === true) {
-        return 'isCommitting is a mutex, hence only changes are allowed';
-      }
-    },
 
-    /**
-     * Checks if a state change can be accepted.
-     *
-     * @param {string} from
-     *   From state.
-     * @param {string} to
-     *   To state.
-     * @param {object} context
-     *   Context for the check.
-     * @param {string} context.reason
-     *   The reason for the state change.
-     * @param {bool} context.confirming
-     *   Whether context is confirming or not.
-     *
-     * @return {bool}
-     *   Whether the state change is accepted or not.
-     *
-     * @see Drupal.quickedit.AppView#acceptEditorStateChange
-     */
-    _acceptStateChange(from, to, context) {
-      let accept = true;
-
-      // In general, enforce the states sequence. Disallow going back from a
-      // "later" state to an "earlier" state, except in explicitly allowed
-      // cases.
-      if (!this.constructor.followsStateSequence(from, to)) {
-        accept = false;
-
-        // Allow: closing -> closed.
-        // Necessary to stop editing an entity.
-        if (from === 'closing' && to === 'closed') {
-          accept = true;
-        }
-        // Allow: committing -> opened.
-        // Necessary to be able to correct an invalid field, or to hit the
-        // "Save" button again after a server/network error.
-        else if (from === 'committing' && to === 'opened' && context.reason && (context.reason === 'invalid' || context.reason === 'networkerror')) {
-          accept = true;
-        }
-        // Allow: deactivating -> opened.
-        // Necessary to be able to confirm changes with the user.
-        else if (from === 'deactivating' && to === 'opened' && context.confirming) {
-          accept = true;
-        }
-        // Allow: opened -> deactivating.
-        // Necessary to be able to stop editing.
-        else if (from === 'opened' && to === 'deactivating' && context.confirmed) {
-          accept = true;
+        // Validate setting isCommitting = true.
+        const currentIsCommitting = this.get('isCommitting');
+        const nextIsCommitting = attrs.isCommitting;
+        if (currentIsCommitting === false && nextIsCommitting === true) {
+          if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) {
+            return 'isCommitting change not accepted because fields are not in acceptable state';
+          }
+        } else if (currentIsCommitting === true && nextIsCommitting === true) {
+          return 'isCommitting is a mutex, hence only changes are allowed';
         }
-      }
+      },
 
-      return accept;
-    },
+      /**
+       * Checks if a state change can be accepted.
+       *
+       * @param {string} from
+       *   From state.
+       * @param {string} to
+       *   To state.
+       * @param {object} context
+       *   Context for the check.
+       * @param {string} context.reason
+       *   The reason for the state change.
+       * @param {bool} context.confirming
+       *   Whether context is confirming or not.
+       *
+       * @return {bool}
+       *   Whether the state change is accepted or not.
+       *
+       * @see Drupal.quickedit.AppView#acceptEditorStateChange
+       */
+      _acceptStateChange(from, to, context) {
+        let accept = true;
 
-    /**
-     * Checks if fields have acceptable states.
-     *
-     * @param {Array} acceptedFieldStates
-     *   An array of acceptable field states to check for.
-     *
-     * @return {bool}
-     *   Whether the fields have an acceptable state.
-     *
-     * @see Drupal.quickedit.EntityModel#validate
-     */
-    _fieldsHaveAcceptableStates(acceptedFieldStates) {
-      let accept = true;
-
-      // If no acceptable field states are provided, assume all field states are
-      // acceptable. We want to let validation pass as a default and only
-      // check validity on calls to set that explicitly request it.
-      if (acceptedFieldStates.length > 0) {
-        const fieldStates = this.get('fields').pluck('state') || [];
-        // If not all fields are in one of the accepted field states, then we
-        // still can't allow this state change.
-        if (_.difference(fieldStates, acceptedFieldStates).length) {
+        // In general, enforce the states sequence. Disallow going back from a
+        // "later" state to an "earlier" state, except in explicitly allowed
+        // cases.
+        if (!this.constructor.followsStateSequence(from, to)) {
           accept = false;
+
+          // Allow: closing -> closed.
+          // Necessary to stop editing an entity.
+          if (from === 'closing' && to === 'closed') {
+            accept = true;
+          }
+          // Allow: committing -> opened.
+          // Necessary to be able to correct an invalid field, or to hit the
+          // "Save" button again after a server/network error.
+          else if (
+            from === 'committing' &&
+            to === 'opened' &&
+            context.reason &&
+            (context.reason === 'invalid' || context.reason === 'networkerror')
+          ) {
+            accept = true;
+          }
+          // Allow: deactivating -> opened.
+          // Necessary to be able to confirm changes with the user.
+          else if (
+            from === 'deactivating' &&
+            to === 'opened' &&
+            context.confirming
+          ) {
+            accept = true;
+          }
+          // Allow: opened -> deactivating.
+          // Necessary to be able to stop editing.
+          else if (
+            from === 'opened' &&
+            to === 'deactivating' &&
+            context.confirmed
+          ) {
+            accept = true;
+          }
         }
-      }
 
-      return accept;
-    },
+        return accept;
+      },
 
-    /**
-     * Destroys the entity model.
-     *
-     * @param {object} options
-     *   Options for the entity model.
-     */
-    destroy(options) {
-      Drupal.quickedit.BaseModel.prototype.destroy.call(this, options);
+      /**
+       * Checks if fields have acceptable states.
+       *
+       * @param {Array} acceptedFieldStates
+       *   An array of acceptable field states to check for.
+       *
+       * @return {bool}
+       *   Whether the fields have an acceptable state.
+       *
+       * @see Drupal.quickedit.EntityModel#validate
+       */
+      _fieldsHaveAcceptableStates(acceptedFieldStates) {
+        let accept = true;
+
+        // If no acceptable field states are provided, assume all field states are
+        // acceptable. We want to let validation pass as a default and only
+        // check validity on calls to set that explicitly request it.
+        if (acceptedFieldStates.length > 0) {
+          const fieldStates = this.get('fields').pluck('state') || [];
+          // If not all fields are in one of the accepted field states, then we
+          // still can't allow this state change.
+          if (_.difference(fieldStates, acceptedFieldStates).length) {
+            accept = false;
+          }
+        }
 
-      this.stopListening();
+        return accept;
+      },
 
-      // Destroy all fields of this entity.
-      this.get('fields').reset();
-    },
+      /**
+       * Destroys the entity model.
+       *
+       * @param {object} options
+       *   Options for the entity model.
+       */
+      destroy(options) {
+        Drupal.quickedit.BaseModel.prototype.destroy.call(this, options);
 
-    /**
-     * @inheritdoc
-     */
-    sync() {
-      // We don't use REST updates to sync.
+        this.stopListening();
 
-    },
+        // Destroy all fields of this entity.
+        this.get('fields').reset();
+      },
 
-  }, /** @lends Drupal.quickedit.EntityModel */{
-
-    /**
-     * Sequence of all possible states an entity can be in during quickediting.
-     *
-     * @type {Array.<string>}
-     */
-    states: [
-      // Initial state, like field's 'inactive' OR the user has just finished
-      // in-place editing this entity.
-      // - Trigger: none (initial) or EntityModel (finished).
-      // - Expected behavior: (when not initial state): tear down
-      //   EntityToolbarView, in-place editors and related views.
-      'closed',
-      // User has activated in-place editing of this entity.
-      // - Trigger: user.
-      // - Expected behavior: the EntityToolbarView is gets set up, in-place
-      //   editors (EditorViews) and related views for this entity's fields are
-      //   set up. Upon completion of those, the state is changed to 'opening'.
-      'launching',
-      // Launching has finished.
-      // - Trigger: application.
-      // - Guarantees: in-place editors ready for use, all entity and field
-      //   views have been set up, all fields are in the 'inactive' state.
-      // - Expected behavior: all fields are changed to the 'candidate' state
-      //   and once this is completed, the entity state will be changed to
-      //   'opened'.
-      'opening',
-      // Opening has finished.
-      // - Trigger: EntityModel.
-      // - Guarantees: see 'opening', all fields are in the 'candidate' state.
-      // - Expected behavior: the user is able to actually use in-place editing.
-      'opened',
-      // User has clicked the 'Save' button (and has thus changed at least one
-      // field).
-      // - Trigger: user.
-      // - Guarantees: see 'opened', plus: either a changed field is in
-      //   PrivateTempStore, or the user has just modified a field without
-      //   activating (switching to) another field.
-      // - Expected behavior: 1) if any of the fields are not yet in
-      //   PrivateTempStore, save them to PrivateTempStore, 2) if then any of
-      //   the fields has the 'invalid' state, then change the entity state back
-      //   to 'opened', otherwise: save the entity by committing it from
-      //   PrivateTempStore into permanent storage.
-      'committing',
-      // User has clicked the 'Close' button, or has clicked the 'Save' button
-      // and that was successfully completed.
-      // - Trigger: user or EntityModel.
-      // - Guarantees: when having clicked 'Close' hardly any: fields may be in
-      //   a variety of states; when having clicked 'Save': all fields are in
-      //   the 'candidate' state.
-      // - Expected behavior: transition all fields to the 'candidate' state,
-      //   possibly requiring confirmation in the case of having clicked
-      //   'Close'.
-      'deactivating',
-      // Deactivation has been completed.
-      // - Trigger: EntityModel.
-      // - Guarantees: all fields are in the 'candidate' state.
-      // - Expected behavior: change all fields to the 'inactive' state.
-      'closing',
-    ],
-
-    /**
-     * Indicates whether the 'from' state comes before the 'to' state.
-     *
-     * @param {string} from
-     *   One of {@link Drupal.quickedit.EntityModel.states}.
-     * @param {string} to
-     *   One of {@link Drupal.quickedit.EntityModel.states}.
-     *
-     * @return {bool}
-     *   Whether the 'from' state comes before the 'to' state.
-     */
-    followsStateSequence(from, to) {
-      return _.indexOf(this.states, from) < _.indexOf(this.states, to);
+      /**
+       * @inheritdoc
+       */
+      sync() {
+        // We don't use REST updates to sync.
+      },
     },
+    /** @lends Drupal.quickedit.EntityModel */ {
+      /**
+       * Sequence of all possible states an entity can be in during quickediting.
+       *
+       * @type {Array.<string>}
+       */
+      states: [
+        // Initial state, like field's 'inactive' OR the user has just finished
+        // in-place editing this entity.
+        // - Trigger: none (initial) or EntityModel (finished).
+        // - Expected behavior: (when not initial state): tear down
+        //   EntityToolbarView, in-place editors and related views.
+        'closed',
+        // User has activated in-place editing of this entity.
+        // - Trigger: user.
+        // - Expected behavior: the EntityToolbarView is gets set up, in-place
+        //   editors (EditorViews) and related views for this entity's fields are
+        //   set up. Upon completion of those, the state is changed to 'opening'.
+        'launching',
+        // Launching has finished.
+        // - Trigger: application.
+        // - Guarantees: in-place editors ready for use, all entity and field
+        //   views have been set up, all fields are in the 'inactive' state.
+        // - Expected behavior: all fields are changed to the 'candidate' state
+        //   and once this is completed, the entity state will be changed to
+        //   'opened'.
+        'opening',
+        // Opening has finished.
+        // - Trigger: EntityModel.
+        // - Guarantees: see 'opening', all fields are in the 'candidate' state.
+        // - Expected behavior: the user is able to actually use in-place editing.
+        'opened',
+        // User has clicked the 'Save' button (and has thus changed at least one
+        // field).
+        // - Trigger: user.
+        // - Guarantees: see 'opened', plus: either a changed field is in
+        //   PrivateTempStore, or the user has just modified a field without
+        //   activating (switching to) another field.
+        // - Expected behavior: 1) if any of the fields are not yet in
+        //   PrivateTempStore, save them to PrivateTempStore, 2) if then any of
+        //   the fields has the 'invalid' state, then change the entity state back
+        //   to 'opened', otherwise: save the entity by committing it from
+        //   PrivateTempStore into permanent storage.
+        'committing',
+        // User has clicked the 'Close' button, or has clicked the 'Save' button
+        // and that was successfully completed.
+        // - Trigger: user or EntityModel.
+        // - Guarantees: when having clicked 'Close' hardly any: fields may be in
+        //   a variety of states; when having clicked 'Save': all fields are in
+        //   the 'candidate' state.
+        // - Expected behavior: transition all fields to the 'candidate' state,
+        //   possibly requiring confirmation in the case of having clicked
+        //   'Close'.
+        'deactivating',
+        // Deactivation has been completed.
+        // - Trigger: EntityModel.
+        // - Guarantees: all fields are in the 'candidate' state.
+        // - Expected behavior: change all fields to the 'inactive' state.
+        'closing',
+      ],
 
-  });
+      /**
+       * Indicates whether the 'from' state comes before the 'to' state.
+       *
+       * @param {string} from
+       *   One of {@link Drupal.quickedit.EntityModel.states}.
+       * @param {string} to
+       *   One of {@link Drupal.quickedit.EntityModel.states}.
+       *
+       * @return {bool}
+       *   Whether the 'from' state comes before the 'to' state.
+       */
+      followsStateSequence(from, to) {
+        return _.indexOf(this.states, from) < _.indexOf(this.states, to);
+      },
+    },
+  );
 
   /**
    * @constructor
    *
    * @augments Backbone.Collection
    */
-  Drupal.quickedit.EntityCollection = Backbone.Collection.extend(/** @lends Drupal.quickedit.EntityCollection# */{
-
-    /**
-     * @type {Drupal.quickedit.EntityModel}
-     */
-    model: Drupal.quickedit.EntityModel,
-  });
-}(_, jQuery, Backbone, Drupal));
+  Drupal.quickedit.EntityCollection = Backbone.Collection.extend(
+    /** @lends Drupal.quickedit.EntityCollection# */ {
+      /**
+       * @type {Drupal.quickedit.EntityModel}
+       */
+      model: Drupal.quickedit.EntityModel,
+    },
+  );
+})(_, jQuery, Backbone, Drupal);