FieldModel.es6.js 12 KB

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