This repository has been archived on 2025-01-19. You can view files and clone it, but cannot push or open issues or pull requests.
drupalcampbristol/core/modules/quickedit/js/models/FieldModel.js

349 lines
12 KiB
JavaScript
Raw Normal View History

/**
* @file
* A Backbone Model for the state of an in-place editable field in the DOM.
*/
(function (_, Backbone, Drupal) {
'use strict';
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
* `<entity type>/<id>/<field name>/<language>/<view mode>`
*
* @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 type>/<id>/<field name>/<language>/<view
* mode>[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
* `<entity type>/<id>/<field name>/<language>`, 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: function (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: function (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: function () {
// We don't use REST updates to sync.
return;
},
/**
* 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: function (attrs, options) {
var current = this.get('state');
var 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 `<entity type>/<id>`.
*/
getEntityID: function () {
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: function () {
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: function () {
var currentField = this;
var 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(function (field) {
// Ignore the current field.
if (field === currentField) {
return;
}
// Also ignore other fields with the same view mode.
else if (field.get('fieldID') === currentField.get('fieldID')) {
return;
}
else {
otherViewModes.push(field.getViewMode());
}
});
return otherViewModes;
}
}, /** @lends Drupal.quickedit.FieldModel */{
/**
* Sequence of all possible states a field can be in during quickediting.
*
* @type {Array.<string>}
*/
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: function (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));