EntityModel.es6.js 27 KB

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