Mercurial > hg > isophonics-drupal-site
comparison core/modules/quickedit/js/models/EntityModel.es6.js @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 1fec387a4317 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
1 /** | |
2 * @file | |
3 * A Backbone Model for the state of an in-place editable entity in the DOM. | |
4 */ | |
5 | |
6 (function (_, $, Backbone, Drupal) { | |
7 Drupal.quickedit.EntityModel = Drupal.quickedit.BaseModel.extend(/** @lends Drupal.quickedit.EntityModel# */{ | |
8 | |
9 /** | |
10 * @type {object} | |
11 */ | |
12 defaults: /** @lends Drupal.quickedit.EntityModel# */{ | |
13 | |
14 /** | |
15 * The DOM element that represents this entity. | |
16 * | |
17 * It may seem bizarre to have a DOM element in a Backbone Model, but we | |
18 * need to be able to map entities in the DOM to EntityModels in memory. | |
19 * | |
20 * @type {HTMLElement} | |
21 */ | |
22 el: null, | |
23 | |
24 /** | |
25 * An entity ID, of the form `<entity type>/<entity ID>` | |
26 * | |
27 * @example | |
28 * "node/1" | |
29 * | |
30 * @type {string} | |
31 */ | |
32 entityID: null, | |
33 | |
34 /** | |
35 * An entity instance ID. | |
36 * | |
37 * The first instance of a specific entity (i.e. with a given entity ID) | |
38 * is assigned 0, the second 1, and so on. | |
39 * | |
40 * @type {number} | |
41 */ | |
42 entityInstanceID: null, | |
43 | |
44 /** | |
45 * The unique ID of this entity instance on the page, of the form | |
46 * `<entity type>/<entity ID>[entity instance ID]` | |
47 * | |
48 * @example | |
49 * "node/1[0]" | |
50 * | |
51 * @type {string} | |
52 */ | |
53 id: null, | |
54 | |
55 /** | |
56 * The label of the entity. | |
57 * | |
58 * @type {string} | |
59 */ | |
60 label: null, | |
61 | |
62 /** | |
63 * A FieldCollection for all fields of the entity. | |
64 * | |
65 * @type {Drupal.quickedit.FieldCollection} | |
66 * | |
67 * @see Drupal.quickedit.FieldCollection | |
68 */ | |
69 fields: null, | |
70 | |
71 // The attributes below are stateful. The ones above will never change | |
72 // during the life of a EntityModel instance. | |
73 | |
74 /** | |
75 * Indicates whether this entity is currently being edited in-place. | |
76 * | |
77 * @type {bool} | |
78 */ | |
79 isActive: false, | |
80 | |
81 /** | |
82 * Whether one or more fields are already been stored in PrivateTempStore. | |
83 * | |
84 * @type {bool} | |
85 */ | |
86 inTempStore: false, | |
87 | |
88 /** | |
89 * Indicates whether a "Save" button is necessary or not. | |
90 * | |
91 * Whether one or more fields have already been stored in PrivateTempStore | |
92 * *or* the field that's currently being edited is in the 'changed' or a | |
93 * later state. | |
94 * | |
95 * @type {bool} | |
96 */ | |
97 isDirty: false, | |
98 | |
99 /** | |
100 * Whether the request to the server has been made to commit this entity. | |
101 * | |
102 * Used to prevent multiple such requests. | |
103 * | |
104 * @type {bool} | |
105 */ | |
106 isCommitting: false, | |
107 | |
108 /** | |
109 * The current processing state of an entity. | |
110 * | |
111 * @type {string} | |
112 */ | |
113 state: 'closed', | |
114 | |
115 /** | |
116 * IDs of fields whose new values have been stored in PrivateTempStore. | |
117 * | |
118 * We must store this on the EntityModel as well (even though it already | |
119 * is on the FieldModel) because when a field is rerendered, its | |
120 * FieldModel is destroyed and this allows us to transition it back to | |
121 * the proper state. | |
122 * | |
123 * @type {Array.<string>} | |
124 */ | |
125 fieldsInTempStore: [], | |
126 | |
127 /** | |
128 * A flag the tells the application that this EntityModel must be reloaded | |
129 * in order to restore the original values to its fields in the client. | |
130 * | |
131 * @type {bool} | |
132 */ | |
133 reload: false, | |
134 }, | |
135 | |
136 /** | |
137 * @constructs | |
138 * | |
139 * @augments Drupal.quickedit.BaseModel | |
140 */ | |
141 initialize() { | |
142 this.set('fields', new Drupal.quickedit.FieldCollection()); | |
143 | |
144 // Respond to entity state changes. | |
145 this.listenTo(this, 'change:state', this.stateChange); | |
146 | |
147 // The state of the entity is largely dependent on the state of its | |
148 // fields. | |
149 this.listenTo(this.get('fields'), 'change:state', this.fieldStateChange); | |
150 | |
151 // Call Drupal.quickedit.BaseModel's initialize() method. | |
152 Drupal.quickedit.BaseModel.prototype.initialize.call(this); | |
153 }, | |
154 | |
155 /** | |
156 * Updates FieldModels' states when an EntityModel change occurs. | |
157 * | |
158 * @param {Drupal.quickedit.EntityModel} entityModel | |
159 * The entity model | |
160 * @param {string} state | |
161 * The state of the associated entity. One of | |
162 * {@link Drupal.quickedit.EntityModel.states}. | |
163 * @param {object} options | |
164 * Options for the entity model. | |
165 */ | |
166 stateChange(entityModel, state, options) { | |
167 const to = state; | |
168 switch (to) { | |
169 case 'closed': | |
170 this.set({ | |
171 isActive: false, | |
172 inTempStore: false, | |
173 isDirty: false, | |
174 }); | |
175 break; | |
176 | |
177 case 'launching': | |
178 break; | |
179 | |
180 case 'opening': | |
181 // Set the fields to candidate state. | |
182 entityModel.get('fields').each((fieldModel) => { | |
183 fieldModel.set('state', 'candidate', options); | |
184 }); | |
185 break; | |
186 | |
187 case 'opened': | |
188 // The entity is now ready for editing! | |
189 this.set('isActive', true); | |
190 break; | |
191 | |
192 case 'committing': | |
193 // The user indicated they want to save the entity. | |
194 var fields = this.get('fields'); | |
195 // For fields that are in an active state, transition them to | |
196 // candidate. | |
197 fields.chain() | |
198 .filter(fieldModel => _.intersection([fieldModel.get('state')], ['active']).length) | |
199 .each((fieldModel) => { | |
200 fieldModel.set('state', 'candidate'); | |
201 }); | |
202 // For fields that are in a changed state, field values must first be | |
203 // stored in PrivateTempStore. | |
204 fields.chain() | |
205 .filter(fieldModel => _.intersection([fieldModel.get('state')], Drupal.quickedit.app.changedFieldStates).length) | |
206 .each((fieldModel) => { | |
207 fieldModel.set('state', 'saving'); | |
208 }); | |
209 break; | |
210 | |
211 case 'deactivating': | |
212 var changedFields = this.get('fields') | |
213 .filter(fieldModel => _.intersection([fieldModel.get('state')], ['changed', 'invalid']).length); | |
214 // If the entity contains unconfirmed or unsaved changes, return the | |
215 // entity to an opened state and ask the user if they would like to | |
216 // save the changes or discard the changes. | |
217 // 1. One of the fields is in a changed state. The changed field | |
218 // might just be a change in the client or it might have been saved | |
219 // to tempstore. | |
220 // 2. The saved flag is empty and the confirmed flag is empty. If | |
221 // the entity has been saved to the server, the fields changed in | |
222 // the client are irrelevant. If the changes are confirmed, then | |
223 // proceed to set the fields to candidate state. | |
224 if ((changedFields.length || this.get('fieldsInTempStore').length) && (!options.saved && !options.confirmed)) { | |
225 // Cancel deactivation until the user confirms save or discard. | |
226 this.set('state', 'opened', { confirming: true }); | |
227 // An action in reaction to state change must be deferred. | |
228 _.defer(() => { | |
229 Drupal.quickedit.app.confirmEntityDeactivation(entityModel); | |
230 }); | |
231 } | |
232 else { | |
233 const invalidFields = this.get('fields') | |
234 .filter(fieldModel => _.intersection([fieldModel.get('state')], ['invalid']).length); | |
235 // Indicate if this EntityModel needs to be reloaded in order to | |
236 // restore the original values of its fields. | |
237 entityModel.set('reload', (this.get('fieldsInTempStore').length || invalidFields.length)); | |
238 // Set all fields to the 'candidate' state. A changed field may have | |
239 // to go through confirmation first. | |
240 entityModel.get('fields').each((fieldModel) => { | |
241 // If the field is already in the candidate state, trigger a | |
242 // change event so that the entityModel can move to the next state | |
243 // in deactivation. | |
244 if (_.intersection([fieldModel.get('state')], ['candidate', 'highlighted']).length) { | |
245 fieldModel.trigger('change:state', fieldModel, fieldModel.get('state'), options); | |
246 } | |
247 else { | |
248 fieldModel.set('state', 'candidate', options); | |
249 } | |
250 }); | |
251 } | |
252 break; | |
253 | |
254 case 'closing': | |
255 // Set all fields to the 'inactive' state. | |
256 options.reason = 'stop'; | |
257 this.get('fields').each((fieldModel) => { | |
258 fieldModel.set({ | |
259 inTempStore: false, | |
260 state: 'inactive', | |
261 }, options); | |
262 }); | |
263 break; | |
264 } | |
265 }, | |
266 | |
267 /** | |
268 * Updates a Field and Entity model's "inTempStore" when appropriate. | |
269 * | |
270 * Helper function. | |
271 * | |
272 * @param {Drupal.quickedit.EntityModel} entityModel | |
273 * The model of the entity for which a field's state attribute has | |
274 * changed. | |
275 * @param {Drupal.quickedit.FieldModel} fieldModel | |
276 * The model of the field whose state attribute has changed. | |
277 * | |
278 * @see Drupal.quickedit.EntityModel#fieldStateChange | |
279 */ | |
280 _updateInTempStoreAttributes(entityModel, fieldModel) { | |
281 const current = fieldModel.get('state'); | |
282 const previous = fieldModel.previous('state'); | |
283 let fieldsInTempStore = entityModel.get('fieldsInTempStore'); | |
284 // If the fieldModel changed to the 'saved' state: remember that this | |
285 // field was saved to PrivateTempStore. | |
286 if (current === 'saved') { | |
287 // Mark the entity as saved in PrivateTempStore, so that we can pass the | |
288 // proper "reset PrivateTempStore" boolean value when communicating with | |
289 // the server. | |
290 entityModel.set('inTempStore', true); | |
291 // Mark the field as saved in PrivateTempStore, so that visual | |
292 // indicators signifying just that may be rendered. | |
293 fieldModel.set('inTempStore', true); | |
294 // Remember that this field is in PrivateTempStore, restore when | |
295 // rerendered. | |
296 fieldsInTempStore.push(fieldModel.get('fieldID')); | |
297 fieldsInTempStore = _.uniq(fieldsInTempStore); | |
298 entityModel.set('fieldsInTempStore', fieldsInTempStore); | |
299 } | |
300 // If the fieldModel changed to the 'candidate' state from the | |
301 // 'inactive' state, then this is a field for this entity that got | |
302 // rerendered. Restore its previous 'inTempStore' attribute value. | |
303 else if (current === 'candidate' && previous === 'inactive') { | |
304 fieldModel.set('inTempStore', _.intersection([fieldModel.get('fieldID')], fieldsInTempStore).length > 0); | |
305 } | |
306 }, | |
307 | |
308 /** | |
309 * Reacts to state changes in this entity's fields. | |
310 * | |
311 * @param {Drupal.quickedit.FieldModel} fieldModel | |
312 * The model of the field whose state attribute changed. | |
313 * @param {string} state | |
314 * The state of the associated field. One of | |
315 * {@link Drupal.quickedit.FieldModel.states}. | |
316 */ | |
317 fieldStateChange(fieldModel, state) { | |
318 const entityModel = this; | |
319 const fieldState = state; | |
320 // Switch on the entityModel state. | |
321 // The EntityModel responds to FieldModel state changes as a function of | |
322 // its state. For example, a field switching back to 'candidate' state | |
323 // when its entity is in the 'opened' state has no effect on the entity. | |
324 // But that same switch back to 'candidate' state of a field when the | |
325 // entity is in the 'committing' state might allow the entity to proceed | |
326 // with the commit flow. | |
327 switch (this.get('state')) { | |
328 case 'closed': | |
329 case 'launching': | |
330 // It should be impossible to reach these: fields can't change state | |
331 // while the entity is closed or still launching. | |
332 break; | |
333 | |
334 case 'opening': | |
335 // We must change the entity to the 'opened' state, but it must first | |
336 // be confirmed that all of its fieldModels have transitioned to the | |
337 // 'candidate' state. | |
338 // We do this here, because this is called every time a fieldModel | |
339 // changes state, hence each time this is called, we get closer to the | |
340 // goal of having all fieldModels in the 'candidate' state. | |
341 // A state change in reaction to another state change must be | |
342 // deferred. | |
343 _.defer(() => { | |
344 entityModel.set('state', 'opened', { | |
345 'accept-field-states': Drupal.quickedit.app.readyFieldStates, | |
346 }); | |
347 }); | |
348 break; | |
349 | |
350 case 'opened': | |
351 // Set the isDirty attribute when appropriate so that it is known when | |
352 // to display the "Save" button in the entity toolbar. | |
353 // Note that once a field has been changed, there's no way to discard | |
354 // that change, hence it will have to be saved into PrivateTempStore, | |
355 // or the in-place editing of this field will have to be stopped | |
356 // completely. In other words: once any field enters the 'changed' | |
357 // field, then for the remainder of the in-place editing session, the | |
358 // entity is by definition dirty. | |
359 if (fieldState === 'changed') { | |
360 entityModel.set('isDirty', true); | |
361 } | |
362 else { | |
363 this._updateInTempStoreAttributes(entityModel, fieldModel); | |
364 } | |
365 break; | |
366 | |
367 case 'committing': | |
368 // If the field save returned a validation error, set the state of the | |
369 // entity back to 'opened'. | |
370 if (fieldState === 'invalid') { | |
371 // A state change in reaction to another state change must be | |
372 // deferred. | |
373 _.defer(() => { | |
374 entityModel.set('state', 'opened', { reason: 'invalid' }); | |
375 }); | |
376 } | |
377 else { | |
378 this._updateInTempStoreAttributes(entityModel, fieldModel); | |
379 } | |
380 | |
381 // Attempt to save the entity. If the entity's fields are not yet all | |
382 // in a ready state, the save will not be processed. | |
383 var options = { | |
384 'accept-field-states': Drupal.quickedit.app.readyFieldStates, | |
385 }; | |
386 if (entityModel.set('isCommitting', true, options)) { | |
387 entityModel.save({ | |
388 success() { | |
389 entityModel.set({ | |
390 state: 'deactivating', | |
391 isCommitting: false, | |
392 }, { saved: true }); | |
393 }, | |
394 error() { | |
395 // Reset the "isCommitting" mutex. | |
396 entityModel.set('isCommitting', false); | |
397 // Change the state back to "opened", to allow the user to hit | |
398 // the "Save" button again. | |
399 entityModel.set('state', 'opened', { reason: 'networkerror' }); | |
400 // Show a modal to inform the user of the network error. | |
401 const message = Drupal.t('Your changes to <q>@entity-title</q> could not be saved, either due to a website problem or a network connection problem.<br>Please try again.', { '@entity-title': entityModel.get('label') }); | |
402 Drupal.quickedit.util.networkErrorModal(Drupal.t('Network problem!'), message); | |
403 }, | |
404 }); | |
405 } | |
406 break; | |
407 | |
408 case 'deactivating': | |
409 // When setting the entity to 'closing', require that all fieldModels | |
410 // are in either the 'candidate' or 'highlighted' state. | |
411 // A state change in reaction to another state change must be | |
412 // deferred. | |
413 _.defer(() => { | |
414 entityModel.set('state', 'closing', { | |
415 'accept-field-states': Drupal.quickedit.app.readyFieldStates, | |
416 }); | |
417 }); | |
418 break; | |
419 | |
420 case 'closing': | |
421 // When setting the entity to 'closed', require that all fieldModels | |
422 // are in the 'inactive' state. | |
423 // A state change in reaction to another state change must be | |
424 // deferred. | |
425 _.defer(() => { | |
426 entityModel.set('state', 'closed', { | |
427 'accept-field-states': ['inactive'], | |
428 }); | |
429 }); | |
430 break; | |
431 } | |
432 }, | |
433 | |
434 /** | |
435 * Fires an AJAX request to the REST save URL for an entity. | |
436 * | |
437 * @param {object} options | |
438 * An object of options that contains: | |
439 * @param {function} [options.success] | |
440 * A function to invoke if the entity is successfully saved. | |
441 */ | |
442 save(options) { | |
443 const entityModel = this; | |
444 | |
445 // Create a Drupal.ajax instance to save the entity. | |
446 const entitySaverAjax = Drupal.ajax({ | |
447 url: Drupal.url(`quickedit/entity/${entityModel.get('entityID')}`), | |
448 error() { | |
449 // Let the Drupal.quickedit.EntityModel Backbone model's error() | |
450 // method handle errors. | |
451 options.error.call(entityModel); | |
452 }, | |
453 }); | |
454 // Entity saved successfully. | |
455 entitySaverAjax.commands.quickeditEntitySaved = function (ajax, response, status) { | |
456 // All fields have been moved from PrivateTempStore to permanent | |
457 // storage, update the "inTempStore" attribute on FieldModels, on the | |
458 // EntityModel and clear EntityModel's "fieldInTempStore" attribute. | |
459 entityModel.get('fields').each((fieldModel) => { | |
460 fieldModel.set('inTempStore', false); | |
461 }); | |
462 entityModel.set('inTempStore', false); | |
463 entityModel.set('fieldsInTempStore', []); | |
464 | |
465 // Invoke the optional success callback. | |
466 if (options.success) { | |
467 options.success.call(entityModel); | |
468 } | |
469 }; | |
470 // Trigger the AJAX request, which will will return the | |
471 // quickeditEntitySaved AJAX command to which we then react. | |
472 entitySaverAjax.execute(); | |
473 }, | |
474 | |
475 /** | |
476 * Validate the entity model. | |
477 * | |
478 * @param {object} attrs | |
479 * The attributes changes in the save or set call. | |
480 * @param {object} options | |
481 * An object with the following option: | |
482 * @param {string} [options.reason] | |
483 * A string that conveys a particular reason to allow for an exceptional | |
484 * state change. | |
485 * @param {Array} options.accept-field-states | |
486 * An array of strings that represent field states that the entities must | |
487 * be in to validate. For example, if `accept-field-states` is | |
488 * `['candidate', 'highlighted']`, then all the fields of the entity must | |
489 * be in either of these two states for the save or set call to | |
490 * validate and proceed. | |
491 * | |
492 * @return {string} | |
493 * A string to say something about the state of the entity model. | |
494 */ | |
495 validate(attrs, options) { | |
496 const acceptedFieldStates = options['accept-field-states'] || []; | |
497 | |
498 // Validate state change. | |
499 const currentState = this.get('state'); | |
500 const nextState = attrs.state; | |
501 if (currentState !== nextState) { | |
502 // Ensure it's a valid state. | |
503 if (_.indexOf(this.constructor.states, nextState) === -1) { | |
504 return `"${nextState}" is an invalid state`; | |
505 } | |
506 | |
507 // Ensure it's a state change that is allowed. | |
508 // Check if the acceptStateChange function accepts it. | |
509 if (!this._acceptStateChange(currentState, nextState, options)) { | |
510 return 'state change not accepted'; | |
511 } | |
512 // If that function accepts it, then ensure all fields are also in an | |
513 // acceptable state. | |
514 else if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) { | |
515 return 'state change not accepted because fields are not in acceptable state'; | |
516 } | |
517 } | |
518 | |
519 // Validate setting isCommitting = true. | |
520 const currentIsCommitting = this.get('isCommitting'); | |
521 const nextIsCommitting = attrs.isCommitting; | |
522 if (currentIsCommitting === false && nextIsCommitting === true) { | |
523 if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) { | |
524 return 'isCommitting change not accepted because fields are not in acceptable state'; | |
525 } | |
526 } | |
527 else if (currentIsCommitting === true && nextIsCommitting === true) { | |
528 return 'isCommitting is a mutex, hence only changes are allowed'; | |
529 } | |
530 }, | |
531 | |
532 /** | |
533 * Checks if a state change can be accepted. | |
534 * | |
535 * @param {string} from | |
536 * From state. | |
537 * @param {string} to | |
538 * To state. | |
539 * @param {object} context | |
540 * Context for the check. | |
541 * @param {string} context.reason | |
542 * The reason for the state change. | |
543 * @param {bool} context.confirming | |
544 * Whether context is confirming or not. | |
545 * | |
546 * @return {bool} | |
547 * Whether the state change is accepted or not. | |
548 * | |
549 * @see Drupal.quickedit.AppView#acceptEditorStateChange | |
550 */ | |
551 _acceptStateChange(from, to, context) { | |
552 let accept = true; | |
553 | |
554 // In general, enforce the states sequence. Disallow going back from a | |
555 // "later" state to an "earlier" state, except in explicitly allowed | |
556 // cases. | |
557 if (!this.constructor.followsStateSequence(from, to)) { | |
558 accept = false; | |
559 | |
560 // Allow: closing -> closed. | |
561 // Necessary to stop editing an entity. | |
562 if (from === 'closing' && to === 'closed') { | |
563 accept = true; | |
564 } | |
565 // Allow: committing -> opened. | |
566 // Necessary to be able to correct an invalid field, or to hit the | |
567 // "Save" button again after a server/network error. | |
568 else if (from === 'committing' && to === 'opened' && context.reason && (context.reason === 'invalid' || context.reason === 'networkerror')) { | |
569 accept = true; | |
570 } | |
571 // Allow: deactivating -> opened. | |
572 // Necessary to be able to confirm changes with the user. | |
573 else if (from === 'deactivating' && to === 'opened' && context.confirming) { | |
574 accept = true; | |
575 } | |
576 // Allow: opened -> deactivating. | |
577 // Necessary to be able to stop editing. | |
578 else if (from === 'opened' && to === 'deactivating' && context.confirmed) { | |
579 accept = true; | |
580 } | |
581 } | |
582 | |
583 return accept; | |
584 }, | |
585 | |
586 /** | |
587 * Checks if fields have acceptable states. | |
588 * | |
589 * @param {Array} acceptedFieldStates | |
590 * An array of acceptable field states to check for. | |
591 * | |
592 * @return {bool} | |
593 * Whether the fields have an acceptable state. | |
594 * | |
595 * @see Drupal.quickedit.EntityModel#validate | |
596 */ | |
597 _fieldsHaveAcceptableStates(acceptedFieldStates) { | |
598 let accept = true; | |
599 | |
600 // If no acceptable field states are provided, assume all field states are | |
601 // acceptable. We want to let validation pass as a default and only | |
602 // check validity on calls to set that explicitly request it. | |
603 if (acceptedFieldStates.length > 0) { | |
604 const fieldStates = this.get('fields').pluck('state') || []; | |
605 // If not all fields are in one of the accepted field states, then we | |
606 // still can't allow this state change. | |
607 if (_.difference(fieldStates, acceptedFieldStates).length) { | |
608 accept = false; | |
609 } | |
610 } | |
611 | |
612 return accept; | |
613 }, | |
614 | |
615 /** | |
616 * Destroys the entity model. | |
617 * | |
618 * @param {object} options | |
619 * Options for the entity model. | |
620 */ | |
621 destroy(options) { | |
622 Drupal.quickedit.BaseModel.prototype.destroy.call(this, options); | |
623 | |
624 this.stopListening(); | |
625 | |
626 // Destroy all fields of this entity. | |
627 this.get('fields').reset(); | |
628 }, | |
629 | |
630 /** | |
631 * @inheritdoc | |
632 */ | |
633 sync() { | |
634 // We don't use REST updates to sync. | |
635 | |
636 }, | |
637 | |
638 }, /** @lends Drupal.quickedit.EntityModel */{ | |
639 | |
640 /** | |
641 * Sequence of all possible states an entity can be in during quickediting. | |
642 * | |
643 * @type {Array.<string>} | |
644 */ | |
645 states: [ | |
646 // Initial state, like field's 'inactive' OR the user has just finished | |
647 // in-place editing this entity. | |
648 // - Trigger: none (initial) or EntityModel (finished). | |
649 // - Expected behavior: (when not initial state): tear down | |
650 // EntityToolbarView, in-place editors and related views. | |
651 'closed', | |
652 // User has activated in-place editing of this entity. | |
653 // - Trigger: user. | |
654 // - Expected behavior: the EntityToolbarView is gets set up, in-place | |
655 // editors (EditorViews) and related views for this entity's fields are | |
656 // set up. Upon completion of those, the state is changed to 'opening'. | |
657 'launching', | |
658 // Launching has finished. | |
659 // - Trigger: application. | |
660 // - Guarantees: in-place editors ready for use, all entity and field | |
661 // views have been set up, all fields are in the 'inactive' state. | |
662 // - Expected behavior: all fields are changed to the 'candidate' state | |
663 // and once this is completed, the entity state will be changed to | |
664 // 'opened'. | |
665 'opening', | |
666 // Opening has finished. | |
667 // - Trigger: EntityModel. | |
668 // - Guarantees: see 'opening', all fields are in the 'candidate' state. | |
669 // - Expected behavior: the user is able to actually use in-place editing. | |
670 'opened', | |
671 // User has clicked the 'Save' button (and has thus changed at least one | |
672 // field). | |
673 // - Trigger: user. | |
674 // - Guarantees: see 'opened', plus: either a changed field is in | |
675 // PrivateTempStore, or the user has just modified a field without | |
676 // activating (switching to) another field. | |
677 // - Expected behavior: 1) if any of the fields are not yet in | |
678 // PrivateTempStore, save them to PrivateTempStore, 2) if then any of | |
679 // the fields has the 'invalid' state, then change the entity state back | |
680 // to 'opened', otherwise: save the entity by committing it from | |
681 // PrivateTempStore into permanent storage. | |
682 'committing', | |
683 // User has clicked the 'Close' button, or has clicked the 'Save' button | |
684 // and that was successfully completed. | |
685 // - Trigger: user or EntityModel. | |
686 // - Guarantees: when having clicked 'Close' hardly any: fields may be in | |
687 // a variety of states; when having clicked 'Save': all fields are in | |
688 // the 'candidate' state. | |
689 // - Expected behavior: transition all fields to the 'candidate' state, | |
690 // possibly requiring confirmation in the case of having clicked | |
691 // 'Close'. | |
692 'deactivating', | |
693 // Deactivation has been completed. | |
694 // - Trigger: EntityModel. | |
695 // - Guarantees: all fields are in the 'candidate' state. | |
696 // - Expected behavior: change all fields to the 'inactive' state. | |
697 'closing', | |
698 ], | |
699 | |
700 /** | |
701 * Indicates whether the 'from' state comes before the 'to' state. | |
702 * | |
703 * @param {string} from | |
704 * One of {@link Drupal.quickedit.EntityModel.states}. | |
705 * @param {string} to | |
706 * One of {@link Drupal.quickedit.EntityModel.states}. | |
707 * | |
708 * @return {bool} | |
709 * Whether the 'from' state comes before the 'to' state. | |
710 */ | |
711 followsStateSequence(from, to) { | |
712 return _.indexOf(this.states, from) < _.indexOf(this.states, to); | |
713 }, | |
714 | |
715 }); | |
716 | |
717 /** | |
718 * @constructor | |
719 * | |
720 * @augments Backbone.Collection | |
721 */ | |
722 Drupal.quickedit.EntityCollection = Backbone.Collection.extend(/** @lends Drupal.quickedit.EntityCollection# */{ | |
723 | |
724 /** | |
725 * @type {Drupal.quickedit.EntityModel} | |
726 */ | |
727 model: Drupal.quickedit.EntityModel, | |
728 }); | |
729 }(_, jQuery, Backbone, Drupal)); |