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