/** * @file * A Backbone Model for the state of an in-place editable field in the DOM. */ (function (_, Backbone, Drupal) { Drupal.quickedit.FieldModel = Drupal.quickedit.BaseModel.extend(/** @lends Drupal.quickedit.FieldModel# */{ /** * @type {object} */ defaults: /** @lends Drupal.quickedit.FieldModel# */{ /** * The DOM element that represents this field. It may seem bizarre to have * a DOM element in a Backbone Model, but we need to be able to map fields * in the DOM to FieldModels in memory. */ el: null, /** * A field ID, of the form * `////` * * @example * "node/1/field_tags/und/full" */ fieldID: null, /** * The unique ID of this field within its entity instance on the page, of * the form `////[entity instance ID]`. * * @example * "node/1/field_tags/und/full[0]" */ id: null, /** * A {@link Drupal.quickedit.EntityModel}. Its "fields" attribute, which * is a FieldCollection, is automatically updated to include this * FieldModel. */ entity: null, /** * This field's metadata as returned by the * QuickEditController::metadata(). */ metadata: null, /** * Callback function for validating changes between states. Receives the * previous state, new state, context, and a callback. */ acceptStateChange: null, /** * A logical field ID, of the form * `///`, i.e. the fieldID without * the view mode, to be able to identify other instances of the same * field on the page but rendered in a different view mode. * * @example * "node/1/field_tags/und". */ logicalFieldID: null, // The attributes below are stateful. The ones above will never change // during the life of a FieldModel instance. /** * In-place editing state of this field. Defaults to the initial state. * Possible values: {@link Drupal.quickedit.FieldModel.states}. */ state: 'inactive', /** * The field is currently in the 'changed' state or one of the following * states in which the field is still changed. */ isChanged: false, /** * Is tracked by the EntityModel, is mirrored here solely for decorative * purposes: so that FieldDecorationView.renderChanged() can react to it. */ inTempStore: false, /** * The full HTML representation of this field (with the element that has * the data-quickedit-field-id as the outer element). Used to propagate * changes from this field to other instances of the same field storage. */ html: null, /** * An object containing the full HTML representations (values) of other * view modes (keys) of this field, for other instances of this field * displayed in a different view mode. */ htmlForOtherViewModes: null, }, /** * State of an in-place editable field in the DOM. * * @constructs * * @augments Drupal.quickedit.BaseModel * * @param {object} options * Options for the field model. */ initialize(options) { // Store the original full HTML representation of this field. this.set('html', options.el.outerHTML); // Enlist field automatically in the associated entity's field collection. this.get('entity').get('fields').add(this); // Automatically generate the logical field ID. this.set('logicalFieldID', this.get('fieldID').split('/').slice(0, 4).join('/')); // Call Drupal.quickedit.BaseModel's initialize() method. Drupal.quickedit.BaseModel.prototype.initialize.call(this, options); }, /** * Destroys the field model. * * @param {object} options * Options for the field model. */ destroy(options) { if (this.get('state') !== 'inactive') { throw new Error('FieldModel cannot be destroyed if it is not inactive state.'); } Drupal.quickedit.BaseModel.prototype.destroy.call(this, options); }, /** * @inheritdoc */ sync() { // We don't use REST updates to sync. }, /** * Validate function for the field model. * * @param {object} attrs * The attributes changes in the save or set call. * @param {object} options * An object with the following option: * @param {string} [options.reason] * A string that conveys a particular reason to allow for an exceptional * state change. * @param {Array} options.accept-field-states * An array of strings that represent field states that the entities must * be in to validate. For example, if `accept-field-states` is * `['candidate', 'highlighted']`, then all the fields of the entity must * be in either of these two states for the save or set call to * validate and proceed. * * @return {string} * A string to say something about the state of the field model. */ validate(attrs, options) { const current = this.get('state'); const next = attrs.state; if (current !== next) { // Ensure it's a valid state. if (_.indexOf(this.constructor.states, next) === -1) { return `"${next}" is an invalid state`; } // Check if the acceptStateChange callback accepts it. if (!this.get('acceptStateChange')(current, next, options, this)) { return 'state change not accepted'; } } }, /** * Extracts the entity ID from this field's ID. * * @return {string} * An entity ID: a string of the format `/`. */ getEntityID() { return this.get('fieldID').split('/').slice(0, 2).join('/'); }, /** * Extracts the view mode ID from this field's ID. * * @return {string} * A view mode ID. */ getViewMode() { return this.get('fieldID').split('/').pop(); }, /** * Find other instances of this field with different view modes. * * @return {Array} * An array containing view mode IDs. */ findOtherViewModes() { const currentField = this; const otherViewModes = []; Drupal.quickedit.collections.fields // Find all instances of fields that display the same logical field // (same entity, same field, just a different instance and maybe a // different view mode). .where({ logicalFieldID: currentField.get('logicalFieldID') }) .forEach((field) => { // Ignore the current field and other fields with the same view mode. if (field !== currentField && field.get('fieldID') !== currentField.get('fieldID')) { otherViewModes.push(field.getViewMode()); } }); return otherViewModes; }, }, /** @lends Drupal.quickedit.FieldModel */{ /** * Sequence of all possible states a field can be in during quickediting. * * @type {Array.} */ states: [ // The field associated with this FieldModel is linked to an EntityModel; // the user can choose to start in-place editing that entity (and // consequently this field). No in-place editor (EditorView) is associated // with this field, because this field is not being in-place edited. // This is both the initial (not yet in-place editing) and the end state // (finished in-place editing). 'inactive', // The user is in-place editing this entity, and this field is a // candidate // for in-place editing. In-place editor should not // - Trigger: user. // - Guarantees: entity is ready, in-place editor (EditorView) is // associated with the field. // - Expected behavior: visual indicators // around the field indicate it is available for in-place editing, no // in-place editor presented yet. 'candidate', // User is highlighting this field. // - Trigger: user. // - Guarantees: see 'candidate'. // - Expected behavior: visual indicators to convey highlighting, in-place // editing toolbar shows field's label. 'highlighted', // User has activated the in-place editing of this field; in-place editor // is activating. // - Trigger: user. // - Guarantees: see 'candidate'. // - Expected behavior: loading indicator, in-place editor is loading // remote data (e.g. retrieve form from back-end). Upon retrieval of // remote data, the in-place editor transitions the field's state to // 'active'. 'activating', // In-place editor has finished loading remote data; ready for use. // - Trigger: in-place editor. // - Guarantees: see 'candidate'. // - Expected behavior: in-place editor for the field is ready for use. 'active', // User has modified values in the in-place editor. // - Trigger: user. // - Guarantees: see 'candidate', plus in-place editor is ready for use. // - Expected behavior: visual indicator of change. 'changed', // User is saving changed field data in in-place editor to // PrivateTempStore. The save mechanism of the in-place editor is called. // - Trigger: user. // - Guarantees: see 'candidate' and 'active'. // - Expected behavior: saving indicator, in-place editor is saving field // data into PrivateTempStore. Upon successful saving (without // validation errors), the in-place editor transitions the field's state // to 'saved', but to 'invalid' upon failed saving (with validation // errors). 'saving', // In-place editor has successfully saved the changed field. // - Trigger: in-place editor. // - Guarantees: see 'candidate' and 'active'. // - Expected behavior: transition back to 'candidate' state because the // deed is done. Then: 1) transition to 'inactive' to allow the field // to be rerendered, 2) destroy the FieldModel (which also destroys // attached views like the EditorView), 3) replace the existing field // HTML with the existing HTML and 4) attach behaviors again so that the // field becomes available again for in-place editing. 'saved', // In-place editor has failed to saved the changed field: there were // validation errors. // - Trigger: in-place editor. // - Guarantees: see 'candidate' and 'active'. // - Expected behavior: remain in 'invalid' state, let the user make more // changes so that he can save it again, without validation errors. 'invalid', ], /** * Indicates whether the 'from' state comes before the 'to' state. * * @param {string} from * One of {@link Drupal.quickedit.FieldModel.states}. * @param {string} to * One of {@link Drupal.quickedit.FieldModel.states}. * * @return {bool} * Whether the 'from' state comes before the 'to' state. */ followsStateSequence(from, to) { return _.indexOf(this.states, from) < _.indexOf(this.states, to); }, }); /** * @constructor * * @augments Backbone.Collection */ Drupal.quickedit.FieldCollection = Backbone.Collection.extend(/** @lends Drupal.quickedit.FieldCollection */{ /** * @type {Drupal.quickedit.FieldModel} */ model: Drupal.quickedit.FieldModel, }); }(_, Backbone, Drupal));