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