X-Git-Url: http://www.aleph1.co.uk/gitweb/?p=yaffs-website;a=blobdiff_plain;f=web%2Fcore%2Fmodules%2Fquickedit%2Fjs%2Fmodels%2FEntityModel.js;h=cf84a515aa0f93678dec31a6a7455d4fec100580;hp=f444fbb9176871bdfcf91fc6913260032b02fcd0;hb=9917807b03b64faf00f6a1f29dcb6eafc454efa5;hpb=aea91e65e895364e460983b890e295aa5d5540a5 diff --git a/web/core/modules/quickedit/js/models/EntityModel.js b/web/core/modules/quickedit/js/models/EntityModel.js index f444fbb91..cf84a515a 100644 --- a/web/core/modules/quickedit/js/models/EntityModel.js +++ b/web/core/modules/quickedit/js/models/EntityModel.js @@ -1,172 +1,50 @@ /** - * @file - * A Backbone Model for the state of an in-place editable entity in the DOM. - */ +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ (function (_, $, Backbone, Drupal) { - - 'use strict'; - - Drupal.quickedit.EntityModel = Drupal.quickedit.BaseModel.extend(/** @lends Drupal.quickedit.EntityModel# */{ - - /** - * @type {object} - */ - 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} - */ + Drupal.quickedit.EntityModel = Drupal.quickedit.BaseModel.extend({ + defaults: { el: null, - /** - * An entity ID, of the form `/` - * - * @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 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.} - */ 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 }, - /** - * @constructs - * - * @augments Drupal.quickedit.BaseModel - */ - initialize: function () { + initialize: function 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: function (entityModel, state, options) { + stateChange: function stateChange(entityModel, state, options) { var to = state; switch (to) { case 'closed': @@ -181,81 +59,53 @@ break; case 'opening': - // Set the fields to candidate state. entityModel.get('fields').each(function (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. var fields = this.get('fields'); - // For fields that are in an active state, transition them to - // candidate. - fields.chain() - .filter(function (fieldModel) { - return _.intersection([fieldModel.get('state')], ['active']).length; - }) - .each(function (fieldModel) { - fieldModel.set('state', 'candidate'); - }); - // For fields that are in a changed state, field values must first be - // stored in PrivateTempStore. - fields.chain() - .filter(function (fieldModel) { - return _.intersection([fieldModel.get('state')], Drupal.quickedit.app.changedFieldStates).length; - }) - .each(function (fieldModel) { - fieldModel.set('state', 'saving'); - }); + + fields.chain().filter(function (fieldModel) { + return _.intersection([fieldModel.get('state')], ['active']).length; + }).each(function (fieldModel) { + fieldModel.set('state', 'candidate'); + }); + + fields.chain().filter(function (fieldModel) { + return _.intersection([fieldModel.get('state')], Drupal.quickedit.app.changedFieldStates).length; + }).each(function (fieldModel) { + fieldModel.set('state', 'saving'); + }); break; case 'deactivating': - var changedFields = this.get('fields') - .filter(function (fieldModel) { - return _.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. + var changedFields = this.get('fields').filter(function (fieldModel) { + return _.intersection([fieldModel.get('state')], ['changed', 'invalid']).length; + }); + + if ((changedFields.length || this.get('fieldsInTempStore').length) && !options.saved && !options.confirmed) { + this.set('state', 'opened', { confirming: true }); + _.defer(function () { Drupal.quickedit.app.confirmEntityDeactivation(entityModel); }); - } - else { - var invalidFields = this.get('fields') - .filter(function (fieldModel) { - return _.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. + } else { + var invalidFields = this.get('fields').filter(function (fieldModel) { + return _.intersection([fieldModel.get('state')], ['invalid']).length; + }); + + entityModel.set('reload', this.get('fieldsInTempStore').length || invalidFields.length); + entityModel.get('fields').each(function (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 { + } else { fieldModel.set('state', 'candidate', options); } }); @@ -263,7 +113,6 @@ break; case 'closing': - // Set all fields to the 'inactive' state. options.reason = 'stop'; this.get('fields').each(function (fieldModel) { fieldModel.set({ @@ -274,83 +123,33 @@ 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: function (entityModel, fieldModel) { + _updateInTempStoreAttributes: function _updateInTempStoreAttributes(entityModel, fieldModel) { var current = fieldModel.get('state'); var previous = fieldModel.previous('state'); var 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); - } + } 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: function (fieldModel, state) { + fieldStateChange: function fieldStateChange(fieldModel, state) { var entityModel = this; var 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(function () { entityModel.set('state', 'opened', { 'accept-field-states': Drupal.quickedit.app.readyFieldStates @@ -359,57 +158,39 @@ 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 { + } 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(function () { - entityModel.set('state', 'opened', {reason: 'invalid'}); + entityModel.set('state', 'opened', { reason: 'invalid' }); }); - } - else { + } 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. var options = { 'accept-field-states': Drupal.quickedit.app.readyFieldStates }; if (entityModel.set('isCommitting', true, options)) { entityModel.save({ - success: function () { + success: function success() { entityModel.set({ state: 'deactivating', isCommitting: false - }, {saved: true}); + }, { saved: true }); }, - error: function () { - // Reset the "isCommitting" mutex. + error: function error() { 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. - var message = Drupal.t('Your changes to @entity-title could not be saved, either due to a website problem or a network connection problem.
Please try again.', {'@entity-title': entityModel.get('label')}); + + entityModel.set('state', 'opened', { reason: 'networkerror' }); + + var message = Drupal.t('Your changes to @entity-title could not be saved, either due to a website problem or a network connection problem.
Please try again.', { '@entity-title': entityModel.get('label') }); Drupal.quickedit.util.networkErrorModal(Drupal.t('Network problem!'), message); } }); @@ -417,10 +198,6 @@ 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(function () { entityModel.set('state', 'closing', { 'accept-field-states': Drupal.quickedit.app.readyFieldStates @@ -429,10 +206,6 @@ 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(function () { entityModel.set('state', 'closed', { 'accept-field-states': ['inactive'] @@ -441,180 +214,82 @@ 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: function (options) { + save: function save(options) { var entityModel = this; - // Create a Drupal.ajax instance to save the entity. var entitySaverAjax = Drupal.ajax({ url: Drupal.url('quickedit/entity/' + entityModel.get('entityID')), - error: function () { - // Let the Drupal.quickedit.EntityModel Backbone model's error() - // method handle errors. + error: function error() { 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(function (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(); }, - - /** - * 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: function (attrs, options) { + validate: function validate(attrs, options) { var acceptedFieldStates = options['accept-field-states'] || []; - // Validate state change. var currentState = this.get('state'); var 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'; - } + } else if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) { + return 'state change not accepted because fields are not in acceptable state'; + } } - // Validate setting isCommitting = true. var currentIsCommitting = this.get('isCommitting'); var 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) { + } 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: function (from, to, context) { + _acceptStateChange: function _acceptStateChange(from, to, context) { var 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; - } + } else if (from === 'committing' && to === 'opened' && context.reason && (context.reason === 'invalid' || context.reason === 'networkerror')) { + accept = true; + } else if (from === 'deactivating' && to === 'opened' && context.confirming) { + accept = true; + } else if (from === 'opened' && to === 'deactivating' && context.confirmed) { + accept = true; + } } return accept; }, - - /** - * 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: function (acceptedFieldStates) { + _fieldsHaveAcceptableStates: function _fieldsHaveAcceptableStates(acceptedFieldStates) { var 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) { var 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; } @@ -622,120 +297,23 @@ return accept; }, - - /** - * Destroys the entity model. - * - * @param {object} options - * Options for the entity model. - */ - destroy: function (options) { + destroy: function destroy(options) { Drupal.quickedit.BaseModel.prototype.destroy.call(this, options); this.stopListening(); - // Destroy all fields of this entity. this.get('fields').reset(); }, + sync: function sync() {} + }, { + states: ['closed', 'launching', 'opening', 'opened', 'committing', 'deactivating', 'closing'], - /** - * @inheritdoc - */ - sync: function () { - // We don't use REST updates to sync. - return; - } - - }, /** @lends Drupal.quickedit.EntityModel */{ - - /** - * Sequence of all possible states an entity can be in during quickediting. - * - * @type {Array.} - */ - 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: function (from, to) { + followsStateSequence: function 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} - */ + Drupal.quickedit.EntityCollection = Backbone.Collection.extend({ model: Drupal.quickedit.EntityModel }); - -}(_, jQuery, Backbone, Drupal)); +})(_, jQuery, Backbone, Drupal); \ No newline at end of file