annotate core/modules/quickedit/js/models/EntityModel.es6.js @ 15:e200cb7efeb3

Update Drupal core to 8.5.3 via Composer
author Chris Cannam
date Thu, 26 Apr 2018 11:26:54 +0100
parents 1fec387a4317
children 129ea1e6d783
rev   line source
Chris@0 1 /**
Chris@0 2 * @file
Chris@0 3 * A Backbone Model for the state of an in-place editable entity in the DOM.
Chris@0 4 */
Chris@0 5
Chris@0 6 (function (_, $, Backbone, Drupal) {
Chris@0 7 Drupal.quickedit.EntityModel = Drupal.quickedit.BaseModel.extend(/** @lends Drupal.quickedit.EntityModel# */{
Chris@0 8
Chris@0 9 /**
Chris@0 10 * @type {object}
Chris@0 11 */
Chris@0 12 defaults: /** @lends Drupal.quickedit.EntityModel# */{
Chris@0 13
Chris@0 14 /**
Chris@0 15 * The DOM element that represents this entity.
Chris@0 16 *
Chris@0 17 * It may seem bizarre to have a DOM element in a Backbone Model, but we
Chris@0 18 * need to be able to map entities in the DOM to EntityModels in memory.
Chris@0 19 *
Chris@0 20 * @type {HTMLElement}
Chris@0 21 */
Chris@0 22 el: null,
Chris@0 23
Chris@0 24 /**
Chris@0 25 * An entity ID, of the form `<entity type>/<entity ID>`
Chris@0 26 *
Chris@0 27 * @example
Chris@0 28 * "node/1"
Chris@0 29 *
Chris@0 30 * @type {string}
Chris@0 31 */
Chris@0 32 entityID: null,
Chris@0 33
Chris@0 34 /**
Chris@0 35 * An entity instance ID.
Chris@0 36 *
Chris@0 37 * The first instance of a specific entity (i.e. with a given entity ID)
Chris@0 38 * is assigned 0, the second 1, and so on.
Chris@0 39 *
Chris@0 40 * @type {number}
Chris@0 41 */
Chris@0 42 entityInstanceID: null,
Chris@0 43
Chris@0 44 /**
Chris@0 45 * The unique ID of this entity instance on the page, of the form
Chris@0 46 * `<entity type>/<entity ID>[entity instance ID]`
Chris@0 47 *
Chris@0 48 * @example
Chris@0 49 * "node/1[0]"
Chris@0 50 *
Chris@0 51 * @type {string}
Chris@0 52 */
Chris@0 53 id: null,
Chris@0 54
Chris@0 55 /**
Chris@0 56 * The label of the entity.
Chris@0 57 *
Chris@0 58 * @type {string}
Chris@0 59 */
Chris@0 60 label: null,
Chris@0 61
Chris@0 62 /**
Chris@0 63 * A FieldCollection for all fields of the entity.
Chris@0 64 *
Chris@0 65 * @type {Drupal.quickedit.FieldCollection}
Chris@0 66 *
Chris@0 67 * @see Drupal.quickedit.FieldCollection
Chris@0 68 */
Chris@0 69 fields: null,
Chris@0 70
Chris@0 71 // The attributes below are stateful. The ones above will never change
Chris@0 72 // during the life of a EntityModel instance.
Chris@0 73
Chris@0 74 /**
Chris@0 75 * Indicates whether this entity is currently being edited in-place.
Chris@0 76 *
Chris@0 77 * @type {bool}
Chris@0 78 */
Chris@0 79 isActive: false,
Chris@0 80
Chris@0 81 /**
Chris@0 82 * Whether one or more fields are already been stored in PrivateTempStore.
Chris@0 83 *
Chris@0 84 * @type {bool}
Chris@0 85 */
Chris@0 86 inTempStore: false,
Chris@0 87
Chris@0 88 /**
Chris@0 89 * Indicates whether a "Save" button is necessary or not.
Chris@0 90 *
Chris@0 91 * Whether one or more fields have already been stored in PrivateTempStore
Chris@0 92 * *or* the field that's currently being edited is in the 'changed' or a
Chris@0 93 * later state.
Chris@0 94 *
Chris@0 95 * @type {bool}
Chris@0 96 */
Chris@0 97 isDirty: false,
Chris@0 98
Chris@0 99 /**
Chris@0 100 * Whether the request to the server has been made to commit this entity.
Chris@0 101 *
Chris@0 102 * Used to prevent multiple such requests.
Chris@0 103 *
Chris@0 104 * @type {bool}
Chris@0 105 */
Chris@0 106 isCommitting: false,
Chris@0 107
Chris@0 108 /**
Chris@0 109 * The current processing state of an entity.
Chris@0 110 *
Chris@0 111 * @type {string}
Chris@0 112 */
Chris@0 113 state: 'closed',
Chris@0 114
Chris@0 115 /**
Chris@0 116 * IDs of fields whose new values have been stored in PrivateTempStore.
Chris@0 117 *
Chris@0 118 * We must store this on the EntityModel as well (even though it already
Chris@0 119 * is on the FieldModel) because when a field is rerendered, its
Chris@0 120 * FieldModel is destroyed and this allows us to transition it back to
Chris@0 121 * the proper state.
Chris@0 122 *
Chris@0 123 * @type {Array.<string>}
Chris@0 124 */
Chris@0 125 fieldsInTempStore: [],
Chris@0 126
Chris@0 127 /**
Chris@0 128 * A flag the tells the application that this EntityModel must be reloaded
Chris@0 129 * in order to restore the original values to its fields in the client.
Chris@0 130 *
Chris@0 131 * @type {bool}
Chris@0 132 */
Chris@0 133 reload: false,
Chris@0 134 },
Chris@0 135
Chris@0 136 /**
Chris@0 137 * @constructs
Chris@0 138 *
Chris@0 139 * @augments Drupal.quickedit.BaseModel
Chris@0 140 */
Chris@0 141 initialize() {
Chris@0 142 this.set('fields', new Drupal.quickedit.FieldCollection());
Chris@0 143
Chris@0 144 // Respond to entity state changes.
Chris@0 145 this.listenTo(this, 'change:state', this.stateChange);
Chris@0 146
Chris@0 147 // The state of the entity is largely dependent on the state of its
Chris@0 148 // fields.
Chris@0 149 this.listenTo(this.get('fields'), 'change:state', this.fieldStateChange);
Chris@0 150
Chris@0 151 // Call Drupal.quickedit.BaseModel's initialize() method.
Chris@0 152 Drupal.quickedit.BaseModel.prototype.initialize.call(this);
Chris@0 153 },
Chris@0 154
Chris@0 155 /**
Chris@0 156 * Updates FieldModels' states when an EntityModel change occurs.
Chris@0 157 *
Chris@0 158 * @param {Drupal.quickedit.EntityModel} entityModel
Chris@0 159 * The entity model
Chris@0 160 * @param {string} state
Chris@0 161 * The state of the associated entity. One of
Chris@0 162 * {@link Drupal.quickedit.EntityModel.states}.
Chris@0 163 * @param {object} options
Chris@0 164 * Options for the entity model.
Chris@0 165 */
Chris@0 166 stateChange(entityModel, state, options) {
Chris@0 167 const to = state;
Chris@0 168 switch (to) {
Chris@0 169 case 'closed':
Chris@0 170 this.set({
Chris@0 171 isActive: false,
Chris@0 172 inTempStore: false,
Chris@0 173 isDirty: false,
Chris@0 174 });
Chris@0 175 break;
Chris@0 176
Chris@0 177 case 'launching':
Chris@0 178 break;
Chris@0 179
Chris@0 180 case 'opening':
Chris@0 181 // Set the fields to candidate state.
Chris@0 182 entityModel.get('fields').each((fieldModel) => {
Chris@0 183 fieldModel.set('state', 'candidate', options);
Chris@0 184 });
Chris@0 185 break;
Chris@0 186
Chris@0 187 case 'opened':
Chris@0 188 // The entity is now ready for editing!
Chris@0 189 this.set('isActive', true);
Chris@0 190 break;
Chris@0 191
Chris@14 192 case 'committing': {
Chris@0 193 // The user indicated they want to save the entity.
Chris@14 194 const fields = this.get('fields');
Chris@0 195 // For fields that are in an active state, transition them to
Chris@0 196 // candidate.
Chris@0 197 fields.chain()
Chris@0 198 .filter(fieldModel => _.intersection([fieldModel.get('state')], ['active']).length)
Chris@0 199 .each((fieldModel) => {
Chris@0 200 fieldModel.set('state', 'candidate');
Chris@0 201 });
Chris@0 202 // For fields that are in a changed state, field values must first be
Chris@0 203 // stored in PrivateTempStore.
Chris@0 204 fields.chain()
Chris@0 205 .filter(fieldModel => _.intersection([fieldModel.get('state')], Drupal.quickedit.app.changedFieldStates).length)
Chris@0 206 .each((fieldModel) => {
Chris@0 207 fieldModel.set('state', 'saving');
Chris@0 208 });
Chris@0 209 break;
Chris@14 210 }
Chris@0 211
Chris@14 212 case 'deactivating': {
Chris@14 213 const changedFields = this.get('fields')
Chris@0 214 .filter(fieldModel => _.intersection([fieldModel.get('state')], ['changed', 'invalid']).length);
Chris@0 215 // If the entity contains unconfirmed or unsaved changes, return the
Chris@0 216 // entity to an opened state and ask the user if they would like to
Chris@0 217 // save the changes or discard the changes.
Chris@0 218 // 1. One of the fields is in a changed state. The changed field
Chris@0 219 // might just be a change in the client or it might have been saved
Chris@0 220 // to tempstore.
Chris@0 221 // 2. The saved flag is empty and the confirmed flag is empty. If
Chris@0 222 // the entity has been saved to the server, the fields changed in
Chris@0 223 // the client are irrelevant. If the changes are confirmed, then
Chris@0 224 // proceed to set the fields to candidate state.
Chris@0 225 if ((changedFields.length || this.get('fieldsInTempStore').length) && (!options.saved && !options.confirmed)) {
Chris@0 226 // Cancel deactivation until the user confirms save or discard.
Chris@0 227 this.set('state', 'opened', { confirming: true });
Chris@0 228 // An action in reaction to state change must be deferred.
Chris@0 229 _.defer(() => {
Chris@0 230 Drupal.quickedit.app.confirmEntityDeactivation(entityModel);
Chris@0 231 });
Chris@0 232 }
Chris@0 233 else {
Chris@0 234 const invalidFields = this.get('fields')
Chris@0 235 .filter(fieldModel => _.intersection([fieldModel.get('state')], ['invalid']).length);
Chris@0 236 // Indicate if this EntityModel needs to be reloaded in order to
Chris@0 237 // restore the original values of its fields.
Chris@0 238 entityModel.set('reload', (this.get('fieldsInTempStore').length || invalidFields.length));
Chris@0 239 // Set all fields to the 'candidate' state. A changed field may have
Chris@0 240 // to go through confirmation first.
Chris@0 241 entityModel.get('fields').each((fieldModel) => {
Chris@0 242 // If the field is already in the candidate state, trigger a
Chris@0 243 // change event so that the entityModel can move to the next state
Chris@0 244 // in deactivation.
Chris@0 245 if (_.intersection([fieldModel.get('state')], ['candidate', 'highlighted']).length) {
Chris@0 246 fieldModel.trigger('change:state', fieldModel, fieldModel.get('state'), options);
Chris@0 247 }
Chris@0 248 else {
Chris@0 249 fieldModel.set('state', 'candidate', options);
Chris@0 250 }
Chris@0 251 });
Chris@0 252 }
Chris@0 253 break;
Chris@14 254 }
Chris@0 255
Chris@0 256 case 'closing':
Chris@0 257 // Set all fields to the 'inactive' state.
Chris@0 258 options.reason = 'stop';
Chris@0 259 this.get('fields').each((fieldModel) => {
Chris@0 260 fieldModel.set({
Chris@0 261 inTempStore: false,
Chris@0 262 state: 'inactive',
Chris@0 263 }, options);
Chris@0 264 });
Chris@0 265 break;
Chris@0 266 }
Chris@0 267 },
Chris@0 268
Chris@0 269 /**
Chris@0 270 * Updates a Field and Entity model's "inTempStore" when appropriate.
Chris@0 271 *
Chris@0 272 * Helper function.
Chris@0 273 *
Chris@0 274 * @param {Drupal.quickedit.EntityModel} entityModel
Chris@0 275 * The model of the entity for which a field's state attribute has
Chris@0 276 * changed.
Chris@0 277 * @param {Drupal.quickedit.FieldModel} fieldModel
Chris@0 278 * The model of the field whose state attribute has changed.
Chris@0 279 *
Chris@0 280 * @see Drupal.quickedit.EntityModel#fieldStateChange
Chris@0 281 */
Chris@0 282 _updateInTempStoreAttributes(entityModel, fieldModel) {
Chris@0 283 const current = fieldModel.get('state');
Chris@0 284 const previous = fieldModel.previous('state');
Chris@0 285 let fieldsInTempStore = entityModel.get('fieldsInTempStore');
Chris@0 286 // If the fieldModel changed to the 'saved' state: remember that this
Chris@0 287 // field was saved to PrivateTempStore.
Chris@0 288 if (current === 'saved') {
Chris@0 289 // Mark the entity as saved in PrivateTempStore, so that we can pass the
Chris@0 290 // proper "reset PrivateTempStore" boolean value when communicating with
Chris@0 291 // the server.
Chris@0 292 entityModel.set('inTempStore', true);
Chris@0 293 // Mark the field as saved in PrivateTempStore, so that visual
Chris@0 294 // indicators signifying just that may be rendered.
Chris@0 295 fieldModel.set('inTempStore', true);
Chris@0 296 // Remember that this field is in PrivateTempStore, restore when
Chris@0 297 // rerendered.
Chris@0 298 fieldsInTempStore.push(fieldModel.get('fieldID'));
Chris@0 299 fieldsInTempStore = _.uniq(fieldsInTempStore);
Chris@0 300 entityModel.set('fieldsInTempStore', fieldsInTempStore);
Chris@0 301 }
Chris@0 302 // If the fieldModel changed to the 'candidate' state from the
Chris@0 303 // 'inactive' state, then this is a field for this entity that got
Chris@0 304 // rerendered. Restore its previous 'inTempStore' attribute value.
Chris@0 305 else if (current === 'candidate' && previous === 'inactive') {
Chris@0 306 fieldModel.set('inTempStore', _.intersection([fieldModel.get('fieldID')], fieldsInTempStore).length > 0);
Chris@0 307 }
Chris@0 308 },
Chris@0 309
Chris@0 310 /**
Chris@0 311 * Reacts to state changes in this entity's fields.
Chris@0 312 *
Chris@0 313 * @param {Drupal.quickedit.FieldModel} fieldModel
Chris@0 314 * The model of the field whose state attribute changed.
Chris@0 315 * @param {string} state
Chris@0 316 * The state of the associated field. One of
Chris@0 317 * {@link Drupal.quickedit.FieldModel.states}.
Chris@0 318 */
Chris@0 319 fieldStateChange(fieldModel, state) {
Chris@0 320 const entityModel = this;
Chris@0 321 const fieldState = state;
Chris@0 322 // Switch on the entityModel state.
Chris@0 323 // The EntityModel responds to FieldModel state changes as a function of
Chris@0 324 // its state. For example, a field switching back to 'candidate' state
Chris@0 325 // when its entity is in the 'opened' state has no effect on the entity.
Chris@0 326 // But that same switch back to 'candidate' state of a field when the
Chris@0 327 // entity is in the 'committing' state might allow the entity to proceed
Chris@0 328 // with the commit flow.
Chris@0 329 switch (this.get('state')) {
Chris@0 330 case 'closed':
Chris@0 331 case 'launching':
Chris@0 332 // It should be impossible to reach these: fields can't change state
Chris@0 333 // while the entity is closed or still launching.
Chris@0 334 break;
Chris@0 335
Chris@0 336 case 'opening':
Chris@0 337 // We must change the entity to the 'opened' state, but it must first
Chris@0 338 // be confirmed that all of its fieldModels have transitioned to the
Chris@0 339 // 'candidate' state.
Chris@0 340 // We do this here, because this is called every time a fieldModel
Chris@0 341 // changes state, hence each time this is called, we get closer to the
Chris@0 342 // goal of having all fieldModels in the 'candidate' state.
Chris@0 343 // A state change in reaction to another state change must be
Chris@0 344 // deferred.
Chris@0 345 _.defer(() => {
Chris@0 346 entityModel.set('state', 'opened', {
Chris@0 347 'accept-field-states': Drupal.quickedit.app.readyFieldStates,
Chris@0 348 });
Chris@0 349 });
Chris@0 350 break;
Chris@0 351
Chris@0 352 case 'opened':
Chris@0 353 // Set the isDirty attribute when appropriate so that it is known when
Chris@0 354 // to display the "Save" button in the entity toolbar.
Chris@0 355 // Note that once a field has been changed, there's no way to discard
Chris@0 356 // that change, hence it will have to be saved into PrivateTempStore,
Chris@0 357 // or the in-place editing of this field will have to be stopped
Chris@0 358 // completely. In other words: once any field enters the 'changed'
Chris@0 359 // field, then for the remainder of the in-place editing session, the
Chris@0 360 // entity is by definition dirty.
Chris@0 361 if (fieldState === 'changed') {
Chris@0 362 entityModel.set('isDirty', true);
Chris@0 363 }
Chris@0 364 else {
Chris@0 365 this._updateInTempStoreAttributes(entityModel, fieldModel);
Chris@0 366 }
Chris@0 367 break;
Chris@0 368
Chris@14 369 case 'committing': {
Chris@0 370 // If the field save returned a validation error, set the state of the
Chris@0 371 // entity back to 'opened'.
Chris@0 372 if (fieldState === 'invalid') {
Chris@0 373 // A state change in reaction to another state change must be
Chris@0 374 // deferred.
Chris@0 375 _.defer(() => {
Chris@0 376 entityModel.set('state', 'opened', { reason: 'invalid' });
Chris@0 377 });
Chris@0 378 }
Chris@0 379 else {
Chris@0 380 this._updateInTempStoreAttributes(entityModel, fieldModel);
Chris@0 381 }
Chris@0 382
Chris@0 383 // Attempt to save the entity. If the entity's fields are not yet all
Chris@0 384 // in a ready state, the save will not be processed.
Chris@14 385 const options = {
Chris@0 386 'accept-field-states': Drupal.quickedit.app.readyFieldStates,
Chris@0 387 };
Chris@0 388 if (entityModel.set('isCommitting', true, options)) {
Chris@0 389 entityModel.save({
Chris@0 390 success() {
Chris@0 391 entityModel.set({
Chris@0 392 state: 'deactivating',
Chris@0 393 isCommitting: false,
Chris@0 394 }, { saved: true });
Chris@0 395 },
Chris@0 396 error() {
Chris@0 397 // Reset the "isCommitting" mutex.
Chris@0 398 entityModel.set('isCommitting', false);
Chris@0 399 // Change the state back to "opened", to allow the user to hit
Chris@0 400 // the "Save" button again.
Chris@0 401 entityModel.set('state', 'opened', { reason: 'networkerror' });
Chris@0 402 // Show a modal to inform the user of the network error.
Chris@0 403 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') });
Chris@0 404 Drupal.quickedit.util.networkErrorModal(Drupal.t('Network problem!'), message);
Chris@0 405 },
Chris@0 406 });
Chris@0 407 }
Chris@0 408 break;
Chris@14 409 }
Chris@0 410
Chris@0 411 case 'deactivating':
Chris@0 412 // When setting the entity to 'closing', require that all fieldModels
Chris@0 413 // are in either the 'candidate' or 'highlighted' state.
Chris@0 414 // A state change in reaction to another state change must be
Chris@0 415 // deferred.
Chris@0 416 _.defer(() => {
Chris@0 417 entityModel.set('state', 'closing', {
Chris@0 418 'accept-field-states': Drupal.quickedit.app.readyFieldStates,
Chris@0 419 });
Chris@0 420 });
Chris@0 421 break;
Chris@0 422
Chris@0 423 case 'closing':
Chris@0 424 // When setting the entity to 'closed', require that all fieldModels
Chris@0 425 // are in the 'inactive' state.
Chris@0 426 // A state change in reaction to another state change must be
Chris@0 427 // deferred.
Chris@0 428 _.defer(() => {
Chris@0 429 entityModel.set('state', 'closed', {
Chris@0 430 'accept-field-states': ['inactive'],
Chris@0 431 });
Chris@0 432 });
Chris@0 433 break;
Chris@0 434 }
Chris@0 435 },
Chris@0 436
Chris@0 437 /**
Chris@0 438 * Fires an AJAX request to the REST save URL for an entity.
Chris@0 439 *
Chris@0 440 * @param {object} options
Chris@0 441 * An object of options that contains:
Chris@0 442 * @param {function} [options.success]
Chris@0 443 * A function to invoke if the entity is successfully saved.
Chris@0 444 */
Chris@0 445 save(options) {
Chris@0 446 const entityModel = this;
Chris@0 447
Chris@0 448 // Create a Drupal.ajax instance to save the entity.
Chris@0 449 const entitySaverAjax = Drupal.ajax({
Chris@0 450 url: Drupal.url(`quickedit/entity/${entityModel.get('entityID')}`),
Chris@0 451 error() {
Chris@0 452 // Let the Drupal.quickedit.EntityModel Backbone model's error()
Chris@0 453 // method handle errors.
Chris@0 454 options.error.call(entityModel);
Chris@0 455 },
Chris@0 456 });
Chris@0 457 // Entity saved successfully.
Chris@0 458 entitySaverAjax.commands.quickeditEntitySaved = function (ajax, response, status) {
Chris@0 459 // All fields have been moved from PrivateTempStore to permanent
Chris@0 460 // storage, update the "inTempStore" attribute on FieldModels, on the
Chris@0 461 // EntityModel and clear EntityModel's "fieldInTempStore" attribute.
Chris@0 462 entityModel.get('fields').each((fieldModel) => {
Chris@0 463 fieldModel.set('inTempStore', false);
Chris@0 464 });
Chris@0 465 entityModel.set('inTempStore', false);
Chris@0 466 entityModel.set('fieldsInTempStore', []);
Chris@0 467
Chris@0 468 // Invoke the optional success callback.
Chris@0 469 if (options.success) {
Chris@0 470 options.success.call(entityModel);
Chris@0 471 }
Chris@0 472 };
Chris@0 473 // Trigger the AJAX request, which will will return the
Chris@0 474 // quickeditEntitySaved AJAX command to which we then react.
Chris@0 475 entitySaverAjax.execute();
Chris@0 476 },
Chris@0 477
Chris@0 478 /**
Chris@0 479 * Validate the entity model.
Chris@0 480 *
Chris@0 481 * @param {object} attrs
Chris@0 482 * The attributes changes in the save or set call.
Chris@0 483 * @param {object} options
Chris@0 484 * An object with the following option:
Chris@0 485 * @param {string} [options.reason]
Chris@0 486 * A string that conveys a particular reason to allow for an exceptional
Chris@0 487 * state change.
Chris@0 488 * @param {Array} options.accept-field-states
Chris@0 489 * An array of strings that represent field states that the entities must
Chris@0 490 * be in to validate. For example, if `accept-field-states` is
Chris@0 491 * `['candidate', 'highlighted']`, then all the fields of the entity must
Chris@0 492 * be in either of these two states for the save or set call to
Chris@0 493 * validate and proceed.
Chris@0 494 *
Chris@0 495 * @return {string}
Chris@0 496 * A string to say something about the state of the entity model.
Chris@0 497 */
Chris@0 498 validate(attrs, options) {
Chris@0 499 const acceptedFieldStates = options['accept-field-states'] || [];
Chris@0 500
Chris@0 501 // Validate state change.
Chris@0 502 const currentState = this.get('state');
Chris@0 503 const nextState = attrs.state;
Chris@0 504 if (currentState !== nextState) {
Chris@0 505 // Ensure it's a valid state.
Chris@0 506 if (_.indexOf(this.constructor.states, nextState) === -1) {
Chris@0 507 return `"${nextState}" is an invalid state`;
Chris@0 508 }
Chris@0 509
Chris@0 510 // Ensure it's a state change that is allowed.
Chris@0 511 // Check if the acceptStateChange function accepts it.
Chris@0 512 if (!this._acceptStateChange(currentState, nextState, options)) {
Chris@0 513 return 'state change not accepted';
Chris@0 514 }
Chris@0 515 // If that function accepts it, then ensure all fields are also in an
Chris@0 516 // acceptable state.
Chris@0 517 else if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) {
Chris@0 518 return 'state change not accepted because fields are not in acceptable state';
Chris@0 519 }
Chris@0 520 }
Chris@0 521
Chris@0 522 // Validate setting isCommitting = true.
Chris@0 523 const currentIsCommitting = this.get('isCommitting');
Chris@0 524 const nextIsCommitting = attrs.isCommitting;
Chris@0 525 if (currentIsCommitting === false && nextIsCommitting === true) {
Chris@0 526 if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) {
Chris@0 527 return 'isCommitting change not accepted because fields are not in acceptable state';
Chris@0 528 }
Chris@0 529 }
Chris@0 530 else if (currentIsCommitting === true && nextIsCommitting === true) {
Chris@0 531 return 'isCommitting is a mutex, hence only changes are allowed';
Chris@0 532 }
Chris@0 533 },
Chris@0 534
Chris@0 535 /**
Chris@0 536 * Checks if a state change can be accepted.
Chris@0 537 *
Chris@0 538 * @param {string} from
Chris@0 539 * From state.
Chris@0 540 * @param {string} to
Chris@0 541 * To state.
Chris@0 542 * @param {object} context
Chris@0 543 * Context for the check.
Chris@0 544 * @param {string} context.reason
Chris@0 545 * The reason for the state change.
Chris@0 546 * @param {bool} context.confirming
Chris@0 547 * Whether context is confirming or not.
Chris@0 548 *
Chris@0 549 * @return {bool}
Chris@0 550 * Whether the state change is accepted or not.
Chris@0 551 *
Chris@0 552 * @see Drupal.quickedit.AppView#acceptEditorStateChange
Chris@0 553 */
Chris@0 554 _acceptStateChange(from, to, context) {
Chris@0 555 let accept = true;
Chris@0 556
Chris@0 557 // In general, enforce the states sequence. Disallow going back from a
Chris@0 558 // "later" state to an "earlier" state, except in explicitly allowed
Chris@0 559 // cases.
Chris@0 560 if (!this.constructor.followsStateSequence(from, to)) {
Chris@0 561 accept = false;
Chris@0 562
Chris@0 563 // Allow: closing -> closed.
Chris@0 564 // Necessary to stop editing an entity.
Chris@0 565 if (from === 'closing' && to === 'closed') {
Chris@0 566 accept = true;
Chris@0 567 }
Chris@0 568 // Allow: committing -> opened.
Chris@0 569 // Necessary to be able to correct an invalid field, or to hit the
Chris@0 570 // "Save" button again after a server/network error.
Chris@0 571 else if (from === 'committing' && to === 'opened' && context.reason && (context.reason === 'invalid' || context.reason === 'networkerror')) {
Chris@0 572 accept = true;
Chris@0 573 }
Chris@0 574 // Allow: deactivating -> opened.
Chris@0 575 // Necessary to be able to confirm changes with the user.
Chris@0 576 else if (from === 'deactivating' && to === 'opened' && context.confirming) {
Chris@0 577 accept = true;
Chris@0 578 }
Chris@0 579 // Allow: opened -> deactivating.
Chris@0 580 // Necessary to be able to stop editing.
Chris@0 581 else if (from === 'opened' && to === 'deactivating' && context.confirmed) {
Chris@0 582 accept = true;
Chris@0 583 }
Chris@0 584 }
Chris@0 585
Chris@0 586 return accept;
Chris@0 587 },
Chris@0 588
Chris@0 589 /**
Chris@0 590 * Checks if fields have acceptable states.
Chris@0 591 *
Chris@0 592 * @param {Array} acceptedFieldStates
Chris@0 593 * An array of acceptable field states to check for.
Chris@0 594 *
Chris@0 595 * @return {bool}
Chris@0 596 * Whether the fields have an acceptable state.
Chris@0 597 *
Chris@0 598 * @see Drupal.quickedit.EntityModel#validate
Chris@0 599 */
Chris@0 600 _fieldsHaveAcceptableStates(acceptedFieldStates) {
Chris@0 601 let accept = true;
Chris@0 602
Chris@0 603 // If no acceptable field states are provided, assume all field states are
Chris@0 604 // acceptable. We want to let validation pass as a default and only
Chris@0 605 // check validity on calls to set that explicitly request it.
Chris@0 606 if (acceptedFieldStates.length > 0) {
Chris@0 607 const fieldStates = this.get('fields').pluck('state') || [];
Chris@0 608 // If not all fields are in one of the accepted field states, then we
Chris@0 609 // still can't allow this state change.
Chris@0 610 if (_.difference(fieldStates, acceptedFieldStates).length) {
Chris@0 611 accept = false;
Chris@0 612 }
Chris@0 613 }
Chris@0 614
Chris@0 615 return accept;
Chris@0 616 },
Chris@0 617
Chris@0 618 /**
Chris@0 619 * Destroys the entity model.
Chris@0 620 *
Chris@0 621 * @param {object} options
Chris@0 622 * Options for the entity model.
Chris@0 623 */
Chris@0 624 destroy(options) {
Chris@0 625 Drupal.quickedit.BaseModel.prototype.destroy.call(this, options);
Chris@0 626
Chris@0 627 this.stopListening();
Chris@0 628
Chris@0 629 // Destroy all fields of this entity.
Chris@0 630 this.get('fields').reset();
Chris@0 631 },
Chris@0 632
Chris@0 633 /**
Chris@0 634 * @inheritdoc
Chris@0 635 */
Chris@0 636 sync() {
Chris@0 637 // We don't use REST updates to sync.
Chris@0 638
Chris@0 639 },
Chris@0 640
Chris@0 641 }, /** @lends Drupal.quickedit.EntityModel */{
Chris@0 642
Chris@0 643 /**
Chris@0 644 * Sequence of all possible states an entity can be in during quickediting.
Chris@0 645 *
Chris@0 646 * @type {Array.<string>}
Chris@0 647 */
Chris@0 648 states: [
Chris@0 649 // Initial state, like field's 'inactive' OR the user has just finished
Chris@0 650 // in-place editing this entity.
Chris@0 651 // - Trigger: none (initial) or EntityModel (finished).
Chris@0 652 // - Expected behavior: (when not initial state): tear down
Chris@0 653 // EntityToolbarView, in-place editors and related views.
Chris@0 654 'closed',
Chris@0 655 // User has activated in-place editing of this entity.
Chris@0 656 // - Trigger: user.
Chris@0 657 // - Expected behavior: the EntityToolbarView is gets set up, in-place
Chris@0 658 // editors (EditorViews) and related views for this entity's fields are
Chris@0 659 // set up. Upon completion of those, the state is changed to 'opening'.
Chris@0 660 'launching',
Chris@0 661 // Launching has finished.
Chris@0 662 // - Trigger: application.
Chris@0 663 // - Guarantees: in-place editors ready for use, all entity and field
Chris@0 664 // views have been set up, all fields are in the 'inactive' state.
Chris@0 665 // - Expected behavior: all fields are changed to the 'candidate' state
Chris@0 666 // and once this is completed, the entity state will be changed to
Chris@0 667 // 'opened'.
Chris@0 668 'opening',
Chris@0 669 // Opening has finished.
Chris@0 670 // - Trigger: EntityModel.
Chris@0 671 // - Guarantees: see 'opening', all fields are in the 'candidate' state.
Chris@0 672 // - Expected behavior: the user is able to actually use in-place editing.
Chris@0 673 'opened',
Chris@0 674 // User has clicked the 'Save' button (and has thus changed at least one
Chris@0 675 // field).
Chris@0 676 // - Trigger: user.
Chris@0 677 // - Guarantees: see 'opened', plus: either a changed field is in
Chris@0 678 // PrivateTempStore, or the user has just modified a field without
Chris@0 679 // activating (switching to) another field.
Chris@0 680 // - Expected behavior: 1) if any of the fields are not yet in
Chris@0 681 // PrivateTempStore, save them to PrivateTempStore, 2) if then any of
Chris@0 682 // the fields has the 'invalid' state, then change the entity state back
Chris@0 683 // to 'opened', otherwise: save the entity by committing it from
Chris@0 684 // PrivateTempStore into permanent storage.
Chris@0 685 'committing',
Chris@0 686 // User has clicked the 'Close' button, or has clicked the 'Save' button
Chris@0 687 // and that was successfully completed.
Chris@0 688 // - Trigger: user or EntityModel.
Chris@0 689 // - Guarantees: when having clicked 'Close' hardly any: fields may be in
Chris@0 690 // a variety of states; when having clicked 'Save': all fields are in
Chris@0 691 // the 'candidate' state.
Chris@0 692 // - Expected behavior: transition all fields to the 'candidate' state,
Chris@0 693 // possibly requiring confirmation in the case of having clicked
Chris@0 694 // 'Close'.
Chris@0 695 'deactivating',
Chris@0 696 // Deactivation has been completed.
Chris@0 697 // - Trigger: EntityModel.
Chris@0 698 // - Guarantees: all fields are in the 'candidate' state.
Chris@0 699 // - Expected behavior: change all fields to the 'inactive' state.
Chris@0 700 'closing',
Chris@0 701 ],
Chris@0 702
Chris@0 703 /**
Chris@0 704 * Indicates whether the 'from' state comes before the 'to' state.
Chris@0 705 *
Chris@0 706 * @param {string} from
Chris@0 707 * One of {@link Drupal.quickedit.EntityModel.states}.
Chris@0 708 * @param {string} to
Chris@0 709 * One of {@link Drupal.quickedit.EntityModel.states}.
Chris@0 710 *
Chris@0 711 * @return {bool}
Chris@0 712 * Whether the 'from' state comes before the 'to' state.
Chris@0 713 */
Chris@0 714 followsStateSequence(from, to) {
Chris@0 715 return _.indexOf(this.states, from) < _.indexOf(this.states, to);
Chris@0 716 },
Chris@0 717
Chris@0 718 });
Chris@0 719
Chris@0 720 /**
Chris@0 721 * @constructor
Chris@0 722 *
Chris@0 723 * @augments Backbone.Collection
Chris@0 724 */
Chris@0 725 Drupal.quickedit.EntityCollection = Backbone.Collection.extend(/** @lends Drupal.quickedit.EntityCollection# */{
Chris@0 726
Chris@0 727 /**
Chris@0 728 * @type {Drupal.quickedit.EntityModel}
Chris@0 729 */
Chris@0 730 model: Drupal.quickedit.EntityModel,
Chris@0 731 });
Chris@0 732 }(_, jQuery, Backbone, Drupal));