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