FieldModel.es6.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. /**
  2. * @file
  3. * A Backbone Model for the state of an in-place editable field in the DOM.
  4. */
  5. (function (_, Backbone, Drupal) {
  6. Drupal.quickedit.FieldModel = Drupal.quickedit.BaseModel.extend(/** @lends Drupal.quickedit.FieldModel# */{
  7. /**
  8. * @type {object}
  9. */
  10. defaults: /** @lends Drupal.quickedit.FieldModel# */{
  11. /**
  12. * The DOM element that represents this field. It may seem bizarre to have
  13. * a DOM element in a Backbone Model, but we need to be able to map fields
  14. * in the DOM to FieldModels in memory.
  15. */
  16. el: null,
  17. /**
  18. * A field ID, of the form
  19. * `<entity type>/<id>/<field name>/<language>/<view mode>`
  20. *
  21. * @example
  22. * "node/1/field_tags/und/full"
  23. */
  24. fieldID: null,
  25. /**
  26. * The unique ID of this field within its entity instance on the page, of
  27. * the form `<entity type>/<id>/<field name>/<language>/<view
  28. * mode>[entity instance ID]`.
  29. *
  30. * @example
  31. * "node/1/field_tags/und/full[0]"
  32. */
  33. id: null,
  34. /**
  35. * A {@link Drupal.quickedit.EntityModel}. Its "fields" attribute, which
  36. * is a FieldCollection, is automatically updated to include this
  37. * FieldModel.
  38. */
  39. entity: null,
  40. /**
  41. * This field's metadata as returned by the
  42. * QuickEditController::metadata().
  43. */
  44. metadata: null,
  45. /**
  46. * Callback function for validating changes between states. Receives the
  47. * previous state, new state, context, and a callback.
  48. */
  49. acceptStateChange: null,
  50. /**
  51. * A logical field ID, of the form
  52. * `<entity type>/<id>/<field name>/<language>`, i.e. the fieldID without
  53. * the view mode, to be able to identify other instances of the same
  54. * field on the page but rendered in a different view mode.
  55. *
  56. * @example
  57. * "node/1/field_tags/und".
  58. */
  59. logicalFieldID: null,
  60. // The attributes below are stateful. The ones above will never change
  61. // during the life of a FieldModel instance.
  62. /**
  63. * In-place editing state of this field. Defaults to the initial state.
  64. * Possible values: {@link Drupal.quickedit.FieldModel.states}.
  65. */
  66. state: 'inactive',
  67. /**
  68. * The field is currently in the 'changed' state or one of the following
  69. * states in which the field is still changed.
  70. */
  71. isChanged: false,
  72. /**
  73. * Is tracked by the EntityModel, is mirrored here solely for decorative
  74. * purposes: so that FieldDecorationView.renderChanged() can react to it.
  75. */
  76. inTempStore: false,
  77. /**
  78. * The full HTML representation of this field (with the element that has
  79. * the data-quickedit-field-id as the outer element). Used to propagate
  80. * changes from this field to other instances of the same field storage.
  81. */
  82. html: null,
  83. /**
  84. * An object containing the full HTML representations (values) of other
  85. * view modes (keys) of this field, for other instances of this field
  86. * displayed in a different view mode.
  87. */
  88. htmlForOtherViewModes: null,
  89. },
  90. /**
  91. * State of an in-place editable field in the DOM.
  92. *
  93. * @constructs
  94. *
  95. * @augments Drupal.quickedit.BaseModel
  96. *
  97. * @param {object} options
  98. * Options for the field model.
  99. */
  100. initialize(options) {
  101. // Store the original full HTML representation of this field.
  102. this.set('html', options.el.outerHTML);
  103. // Enlist field automatically in the associated entity's field collection.
  104. this.get('entity').get('fields').add(this);
  105. // Automatically generate the logical field ID.
  106. this.set('logicalFieldID', this.get('fieldID').split('/').slice(0, 4).join('/'));
  107. // Call Drupal.quickedit.BaseModel's initialize() method.
  108. Drupal.quickedit.BaseModel.prototype.initialize.call(this, options);
  109. },
  110. /**
  111. * Destroys the field model.
  112. *
  113. * @param {object} options
  114. * Options for the field model.
  115. */
  116. destroy(options) {
  117. if (this.get('state') !== 'inactive') {
  118. throw new Error('FieldModel cannot be destroyed if it is not inactive state.');
  119. }
  120. Drupal.quickedit.BaseModel.prototype.destroy.call(this, options);
  121. },
  122. /**
  123. * @inheritdoc
  124. */
  125. sync() {
  126. // We don't use REST updates to sync.
  127. },
  128. /**
  129. * Validate function for the field model.
  130. *
  131. * @param {object} attrs
  132. * The attributes changes in the save or set call.
  133. * @param {object} options
  134. * An object with the following option:
  135. * @param {string} [options.reason]
  136. * A string that conveys a particular reason to allow for an exceptional
  137. * state change.
  138. * @param {Array} options.accept-field-states
  139. * An array of strings that represent field states that the entities must
  140. * be in to validate. For example, if `accept-field-states` is
  141. * `['candidate', 'highlighted']`, then all the fields of the entity must
  142. * be in either of these two states for the save or set call to
  143. * validate and proceed.
  144. *
  145. * @return {string}
  146. * A string to say something about the state of the field model.
  147. */
  148. validate(attrs, options) {
  149. const current = this.get('state');
  150. const next = attrs.state;
  151. if (current !== next) {
  152. // Ensure it's a valid state.
  153. if (_.indexOf(this.constructor.states, next) === -1) {
  154. return `"${next}" is an invalid state`;
  155. }
  156. // Check if the acceptStateChange callback accepts it.
  157. if (!this.get('acceptStateChange')(current, next, options, this)) {
  158. return 'state change not accepted';
  159. }
  160. }
  161. },
  162. /**
  163. * Extracts the entity ID from this field's ID.
  164. *
  165. * @return {string}
  166. * An entity ID: a string of the format `<entity type>/<id>`.
  167. */
  168. getEntityID() {
  169. return this.get('fieldID').split('/').slice(0, 2).join('/');
  170. },
  171. /**
  172. * Extracts the view mode ID from this field's ID.
  173. *
  174. * @return {string}
  175. * A view mode ID.
  176. */
  177. getViewMode() {
  178. return this.get('fieldID').split('/').pop();
  179. },
  180. /**
  181. * Find other instances of this field with different view modes.
  182. *
  183. * @return {Array}
  184. * An array containing view mode IDs.
  185. */
  186. findOtherViewModes() {
  187. const currentField = this;
  188. const otherViewModes = [];
  189. Drupal.quickedit.collections.fields
  190. // Find all instances of fields that display the same logical field
  191. // (same entity, same field, just a different instance and maybe a
  192. // different view mode).
  193. .where({ logicalFieldID: currentField.get('logicalFieldID') })
  194. .forEach((field) => {
  195. // Ignore the current field and other fields with the same view mode.
  196. if (field !== currentField && field.get('fieldID') !== currentField.get('fieldID')) {
  197. otherViewModes.push(field.getViewMode());
  198. }
  199. });
  200. return otherViewModes;
  201. },
  202. }, /** @lends Drupal.quickedit.FieldModel */{
  203. /**
  204. * Sequence of all possible states a field can be in during quickediting.
  205. *
  206. * @type {Array.<string>}
  207. */
  208. states: [
  209. // The field associated with this FieldModel is linked to an EntityModel;
  210. // the user can choose to start in-place editing that entity (and
  211. // consequently this field). No in-place editor (EditorView) is associated
  212. // with this field, because this field is not being in-place edited.
  213. // This is both the initial (not yet in-place editing) and the end state
  214. // (finished in-place editing).
  215. 'inactive',
  216. // The user is in-place editing this entity, and this field is a
  217. // candidate
  218. // for in-place editing. In-place editor should not
  219. // - Trigger: user.
  220. // - Guarantees: entity is ready, in-place editor (EditorView) is
  221. // associated with the field.
  222. // - Expected behavior: visual indicators
  223. // around the field indicate it is available for in-place editing, no
  224. // in-place editor presented yet.
  225. 'candidate',
  226. // User is highlighting this field.
  227. // - Trigger: user.
  228. // - Guarantees: see 'candidate'.
  229. // - Expected behavior: visual indicators to convey highlighting, in-place
  230. // editing toolbar shows field's label.
  231. 'highlighted',
  232. // User has activated the in-place editing of this field; in-place editor
  233. // is activating.
  234. // - Trigger: user.
  235. // - Guarantees: see 'candidate'.
  236. // - Expected behavior: loading indicator, in-place editor is loading
  237. // remote data (e.g. retrieve form from back-end). Upon retrieval of
  238. // remote data, the in-place editor transitions the field's state to
  239. // 'active'.
  240. 'activating',
  241. // In-place editor has finished loading remote data; ready for use.
  242. // - Trigger: in-place editor.
  243. // - Guarantees: see 'candidate'.
  244. // - Expected behavior: in-place editor for the field is ready for use.
  245. 'active',
  246. // User has modified values in the in-place editor.
  247. // - Trigger: user.
  248. // - Guarantees: see 'candidate', plus in-place editor is ready for use.
  249. // - Expected behavior: visual indicator of change.
  250. 'changed',
  251. // User is saving changed field data in in-place editor to
  252. // PrivateTempStore. The save mechanism of the in-place editor is called.
  253. // - Trigger: user.
  254. // - Guarantees: see 'candidate' and 'active'.
  255. // - Expected behavior: saving indicator, in-place editor is saving field
  256. // data into PrivateTempStore. Upon successful saving (without
  257. // validation errors), the in-place editor transitions the field's state
  258. // to 'saved', but to 'invalid' upon failed saving (with validation
  259. // errors).
  260. 'saving',
  261. // In-place editor has successfully saved the changed field.
  262. // - Trigger: in-place editor.
  263. // - Guarantees: see 'candidate' and 'active'.
  264. // - Expected behavior: transition back to 'candidate' state because the
  265. // deed is done. Then: 1) transition to 'inactive' to allow the field
  266. // to be rerendered, 2) destroy the FieldModel (which also destroys
  267. // attached views like the EditorView), 3) replace the existing field
  268. // HTML with the existing HTML and 4) attach behaviors again so that the
  269. // field becomes available again for in-place editing.
  270. 'saved',
  271. // In-place editor has failed to saved the changed field: there were
  272. // validation errors.
  273. // - Trigger: in-place editor.
  274. // - Guarantees: see 'candidate' and 'active'.
  275. // - Expected behavior: remain in 'invalid' state, let the user make more
  276. // changes so that he can save it again, without validation errors.
  277. 'invalid',
  278. ],
  279. /**
  280. * Indicates whether the 'from' state comes before the 'to' state.
  281. *
  282. * @param {string} from
  283. * One of {@link Drupal.quickedit.FieldModel.states}.
  284. * @param {string} to
  285. * One of {@link Drupal.quickedit.FieldModel.states}.
  286. *
  287. * @return {bool}
  288. * Whether the 'from' state comes before the 'to' state.
  289. */
  290. followsStateSequence(from, to) {
  291. return _.indexOf(this.states, from) < _.indexOf(this.states, to);
  292. },
  293. });
  294. /**
  295. * @constructor
  296. *
  297. * @augments Backbone.Collection
  298. */
  299. Drupal.quickedit.FieldCollection = Backbone.Collection.extend(/** @lends Drupal.quickedit.FieldCollection */{
  300. /**
  301. * @type {Drupal.quickedit.FieldModel}
  302. */
  303. model: Drupal.quickedit.FieldModel,
  304. });
  305. }(_, Backbone, Drupal));