261 lines
10 KiB
JavaScript
261 lines
10 KiB
JavaScript
/**
|
|
* @file
|
|
* A Backbone Model for the state of an in-place editable field in the DOM.
|
|
*/
|
|
|
|
(function (_, Backbone, Drupal) {
|
|
|
|
"use strict";
|
|
|
|
/**
|
|
* State of an in-place editable field in the DOM.
|
|
*/
|
|
Drupal.quickedit.FieldModel = Drupal.quickedit.BaseModel.extend({
|
|
|
|
defaults: {
|
|
// 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>", e.g.
|
|
// "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]",
|
|
// e.g. "node/1/field_tags/und/full[0]".
|
|
id: null,
|
|
// A 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. e.g. "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: @see 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
|
|
},
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
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);
|
|
},
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
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;
|
|
}
|
|
|
|
}, {
|
|
|
|
/**
|
|
* A list (sequence) of all possible states a field can be in during in-place
|
|
* editing.
|
|
*/
|
|
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 TempStore. 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 TempStore. 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 Drupal.quickedit.FieldModel.states.
|
|
* @param String to
|
|
* One of Drupal.quickedit.FieldModel.states.
|
|
* @return Boolean
|
|
*/
|
|
followsStateSequence: function (from, to) {
|
|
return _.indexOf(this.states, from) < _.indexOf(this.states, to);
|
|
}
|
|
|
|
});
|
|
|
|
Drupal.quickedit.FieldCollection = Backbone.Collection.extend({
|
|
model: Drupal.quickedit.FieldModel
|
|
});
|
|
|
|
}(_, Backbone, Drupal));
|