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