EntityModel.es6.js 29 KB

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