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