3 * A Backbone Model for the state of an in-place editable field in the DOM.
6 (function (_, Backbone, Drupal) {
10 Drupal.quickedit.FieldModel = Drupal.quickedit.BaseModel.extend(/** @lends Drupal.quickedit.FieldModel# */{
15 defaults: /** @lends Drupal.quickedit.FieldModel# */{
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.
25 * A field ID, of the form
26 * `<entity type>/<id>/<field name>/<language>/<view mode>`
29 * "node/1/field_tags/und/full"
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]`.
39 * "node/1/field_tags/und/full[0]"
44 * A {@link Drupal.quickedit.EntityModel}. Its "fields" attribute, which
45 * is a FieldCollection, is automatically updated to include this
51 * This field's metadata as returned by the
52 * QuickEditController::metadata().
57 * Callback function for validating changes between states. Receives the
58 * previous state, new state, context, and a callback.
60 acceptStateChange: null,
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.
69 * "node/1/field_tags/und".
73 // The attributes below are stateful. The ones above will never change
74 // during the life of a FieldModel instance.
77 * In-place editing state of this field. Defaults to the initial state.
78 * Possible values: {@link Drupal.quickedit.FieldModel.states}.
83 * The field is currently in the 'changed' state or one of the following
84 * states in which the field is still changed.
89 * Is tracked by the EntityModel, is mirrored here solely for decorative
90 * purposes: so that FieldDecorationView.renderChanged() can react to it.
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.
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.
106 htmlForOtherViewModes: null
110 * State of an in-place editable field in the DOM.
114 * @augments Drupal.quickedit.BaseModel
116 * @param {object} options
117 * Options for the field model.
119 initialize: function (options) {
120 // Store the original full HTML representation of this field.
121 this.set('html', options.el.outerHTML);
123 // Enlist field automatically in the associated entity's field collection.
124 this.get('entity').get('fields').add(this);
126 // Automatically generate the logical field ID.
127 this.set('logicalFieldID', this.get('fieldID').split('/').slice(0, 4).join('/'));
129 // Call Drupal.quickedit.BaseModel's initialize() method.
130 Drupal.quickedit.BaseModel.prototype.initialize.call(this, options);
134 * Destroys the field model.
136 * @param {object} options
137 * Options for the field model.
139 destroy: function (options) {
140 if (this.get('state') !== 'inactive') {
141 throw new Error('FieldModel cannot be destroyed if it is not inactive state.');
143 Drupal.quickedit.BaseModel.prototype.destroy.call(this, options);
150 // We don't use REST updates to sync.
155 * Validate function for the field model.
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
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.
172 * A string to say something about the state of the field model.
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';
182 // Check if the acceptStateChange callback accepts it.
183 if (!this.get('acceptStateChange')(current, next, options, this)) {
184 return 'state change not accepted';
190 * Extracts the entity ID from this field's ID.
193 * An entity ID: a string of the format `<entity type>/<id>`.
195 getEntityID: function () {
196 return this.get('fieldID').split('/').slice(0, 2).join('/');
200 * Extracts the view mode ID from this field's ID.
205 getViewMode: function () {
206 return this.get('fieldID').split('/').pop();
210 * Find other instances of this field with different view modes.
213 * An array containing view mode IDs.
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) {
228 // Also ignore other fields with the same view mode.
229 else if (field.get('fieldID') === currentField.get('fieldID')) {
233 otherViewModes.push(field.getViewMode());
236 return otherViewModes;
239 }, /** @lends Drupal.quickedit.FieldModel */{
242 * Sequence of all possible states a field can be in during quickediting.
244 * @type {Array.<string>}
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).
254 // The user is in-place editing this entity, and this field is a
256 // for in-place editing. In-place editor should not
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.
264 // User is highlighting this field.
266 // - Guarantees: see 'candidate'.
267 // - Expected behavior: visual indicators to convey highlighting, in-place
268 // editing toolbar shows field's label.
270 // User has activated the in-place editing of this field; in-place editor
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
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.
284 // User has modified values in the in-place editor.
286 // - Guarantees: see 'candidate', plus in-place editor is ready for use.
287 // - Expected behavior: visual indicator of change.
289 // User is saving changed field data in in-place editor to
290 // PrivateTempStore. The save mechanism of the in-place editor is called.
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
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.
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.
319 * Indicates whether the 'from' state comes before the 'to' state.
321 * @param {string} from
322 * One of {@link Drupal.quickedit.FieldModel.states}.
324 * One of {@link Drupal.quickedit.FieldModel.states}.
327 * Whether the 'from' state comes before the 'to' state.
329 followsStateSequence: function (from, to) {
330 return _.indexOf(this.states, from) < _.indexOf(this.states, to);
338 * @augments Backbone.Collection
340 Drupal.quickedit.FieldCollection = Backbone.Collection.extend(/** @lends Drupal.quickedit.FieldCollection */{
343 * @type {Drupal.quickedit.FieldModel}
345 model: Drupal.quickedit.FieldModel
348 }(_, Backbone, Drupal));