annotate core/modules/quickedit/js/models/FieldModel.es6.js @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@0 1 /**
Chris@0 2 * @file
Chris@0 3 * A Backbone Model for the state of an in-place editable field in the DOM.
Chris@0 4 */
Chris@0 5
Chris@17 6 (function(_, Backbone, Drupal) {
Chris@17 7 Drupal.quickedit.FieldModel = Drupal.quickedit.BaseModel.extend(
Chris@17 8 /** @lends Drupal.quickedit.FieldModel# */ {
Chris@17 9 /**
Chris@17 10 * @type {object}
Chris@17 11 */
Chris@17 12 defaults: /** @lends Drupal.quickedit.FieldModel# */ {
Chris@17 13 /**
Chris@17 14 * The DOM element that represents this field. It may seem bizarre to have
Chris@17 15 * a DOM element in a Backbone Model, but we need to be able to map fields
Chris@17 16 * in the DOM to FieldModels in memory.
Chris@17 17 */
Chris@17 18 el: null,
Chris@0 19
Chris@17 20 /**
Chris@17 21 * A field ID, of the form
Chris@17 22 * `<entity type>/<id>/<field name>/<language>/<view mode>`
Chris@17 23 *
Chris@17 24 * @example
Chris@17 25 * "node/1/field_tags/und/full"
Chris@17 26 */
Chris@17 27 fieldID: null,
Chris@17 28
Chris@17 29 /**
Chris@17 30 * The unique ID of this field within its entity instance on the page, of
Chris@17 31 * the form `<entity type>/<id>/<field name>/<language>/<view
Chris@17 32 * mode>[entity instance ID]`.
Chris@17 33 *
Chris@17 34 * @example
Chris@17 35 * "node/1/field_tags/und/full[0]"
Chris@17 36 */
Chris@17 37 id: null,
Chris@17 38
Chris@17 39 /**
Chris@17 40 * A {@link Drupal.quickedit.EntityModel}. Its "fields" attribute, which
Chris@17 41 * is a FieldCollection, is automatically updated to include this
Chris@17 42 * FieldModel.
Chris@17 43 */
Chris@17 44 entity: null,
Chris@17 45
Chris@17 46 /**
Chris@17 47 * This field's metadata as returned by the
Chris@17 48 * QuickEditController::metadata().
Chris@17 49 */
Chris@17 50 metadata: null,
Chris@17 51
Chris@17 52 /**
Chris@17 53 * Callback function for validating changes between states. Receives the
Chris@17 54 * previous state, new state, context, and a callback.
Chris@17 55 */
Chris@17 56 acceptStateChange: null,
Chris@17 57
Chris@17 58 /**
Chris@17 59 * A logical field ID, of the form
Chris@17 60 * `<entity type>/<id>/<field name>/<language>`, i.e. the fieldID without
Chris@17 61 * the view mode, to be able to identify other instances of the same
Chris@17 62 * field on the page but rendered in a different view mode.
Chris@17 63 *
Chris@17 64 * @example
Chris@17 65 * "node/1/field_tags/und".
Chris@17 66 */
Chris@17 67 logicalFieldID: null,
Chris@17 68
Chris@17 69 // The attributes below are stateful. The ones above will never change
Chris@17 70 // during the life of a FieldModel instance.
Chris@17 71
Chris@17 72 /**
Chris@17 73 * In-place editing state of this field. Defaults to the initial state.
Chris@17 74 * Possible values: {@link Drupal.quickedit.FieldModel.states}.
Chris@17 75 */
Chris@17 76 state: 'inactive',
Chris@17 77
Chris@17 78 /**
Chris@17 79 * The field is currently in the 'changed' state or one of the following
Chris@17 80 * states in which the field is still changed.
Chris@17 81 */
Chris@17 82 isChanged: false,
Chris@17 83
Chris@17 84 /**
Chris@17 85 * Is tracked by the EntityModel, is mirrored here solely for decorative
Chris@17 86 * purposes: so that FieldDecorationView.renderChanged() can react to it.
Chris@17 87 */
Chris@17 88 inTempStore: false,
Chris@17 89
Chris@17 90 /**
Chris@17 91 * The full HTML representation of this field (with the element that has
Chris@17 92 * the data-quickedit-field-id as the outer element). Used to propagate
Chris@17 93 * changes from this field to other instances of the same field storage.
Chris@17 94 */
Chris@17 95 html: null,
Chris@17 96
Chris@17 97 /**
Chris@17 98 * An object containing the full HTML representations (values) of other
Chris@17 99 * view modes (keys) of this field, for other instances of this field
Chris@17 100 * displayed in a different view mode.
Chris@17 101 */
Chris@17 102 htmlForOtherViewModes: null,
Chris@17 103 },
Chris@0 104
Chris@0 105 /**
Chris@17 106 * State of an in-place editable field in the DOM.
Chris@17 107 *
Chris@17 108 * @constructs
Chris@17 109 *
Chris@17 110 * @augments Drupal.quickedit.BaseModel
Chris@17 111 *
Chris@17 112 * @param {object} options
Chris@17 113 * Options for the field model.
Chris@0 114 */
Chris@17 115 initialize(options) {
Chris@17 116 // Store the original full HTML representation of this field.
Chris@17 117 this.set('html', options.el.outerHTML);
Chris@17 118
Chris@17 119 // Enlist field automatically in the associated entity's field collection.
Chris@17 120 this.get('entity')
Chris@17 121 .get('fields')
Chris@17 122 .add(this);
Chris@17 123
Chris@17 124 // Automatically generate the logical field ID.
Chris@17 125 this.set(
Chris@17 126 'logicalFieldID',
Chris@17 127 this.get('fieldID')
Chris@17 128 .split('/')
Chris@17 129 .slice(0, 4)
Chris@17 130 .join('/'),
Chris@17 131 );
Chris@17 132
Chris@17 133 // Call Drupal.quickedit.BaseModel's initialize() method.
Chris@17 134 Drupal.quickedit.BaseModel.prototype.initialize.call(this, options);
Chris@17 135 },
Chris@0 136
Chris@0 137 /**
Chris@17 138 * Destroys the field model.
Chris@0 139 *
Chris@17 140 * @param {object} options
Chris@17 141 * Options for the field model.
Chris@0 142 */
Chris@17 143 destroy(options) {
Chris@17 144 if (this.get('state') !== 'inactive') {
Chris@17 145 throw new Error(
Chris@17 146 'FieldModel cannot be destroyed if it is not inactive state.',
Chris@17 147 );
Chris@17 148 }
Chris@17 149 Drupal.quickedit.BaseModel.prototype.destroy.call(this, options);
Chris@17 150 },
Chris@0 151
Chris@0 152 /**
Chris@17 153 * @inheritdoc
Chris@0 154 */
Chris@17 155 sync() {
Chris@17 156 // We don't use REST updates to sync.
Chris@17 157 },
Chris@0 158
Chris@0 159 /**
Chris@17 160 * Validate function for the field model.
Chris@17 161 *
Chris@17 162 * @param {object} attrs
Chris@17 163 * The attributes changes in the save or set call.
Chris@17 164 * @param {object} options
Chris@17 165 * An object with the following option:
Chris@17 166 * @param {string} [options.reason]
Chris@17 167 * A string that conveys a particular reason to allow for an exceptional
Chris@17 168 * state change.
Chris@17 169 * @param {Array} options.accept-field-states
Chris@17 170 * An array of strings that represent field states that the entities must
Chris@17 171 * be in to validate. For example, if `accept-field-states` is
Chris@17 172 * `['candidate', 'highlighted']`, then all the fields of the entity must
Chris@17 173 * be in either of these two states for the save or set call to
Chris@17 174 * validate and proceed.
Chris@17 175 *
Chris@17 176 * @return {string}
Chris@17 177 * A string to say something about the state of the field model.
Chris@0 178 */
Chris@17 179 validate(attrs, options) {
Chris@17 180 const current = this.get('state');
Chris@17 181 const next = attrs.state;
Chris@17 182 if (current !== next) {
Chris@17 183 // Ensure it's a valid state.
Chris@17 184 if (_.indexOf(this.constructor.states, next) === -1) {
Chris@17 185 return `"${next}" is an invalid state`;
Chris@17 186 }
Chris@17 187 // Check if the acceptStateChange callback accepts it.
Chris@17 188 if (!this.get('acceptStateChange')(current, next, options, this)) {
Chris@17 189 return 'state change not accepted';
Chris@17 190 }
Chris@17 191 }
Chris@17 192 },
Chris@0 193
Chris@0 194 /**
Chris@17 195 * Extracts the entity ID from this field's ID.
Chris@17 196 *
Chris@17 197 * @return {string}
Chris@17 198 * An entity ID: a string of the format `<entity type>/<id>`.
Chris@0 199 */
Chris@17 200 getEntityID() {
Chris@17 201 return this.get('fieldID')
Chris@17 202 .split('/')
Chris@17 203 .slice(0, 2)
Chris@17 204 .join('/');
Chris@17 205 },
Chris@0 206
Chris@0 207 /**
Chris@17 208 * Extracts the view mode ID from this field's ID.
Chris@17 209 *
Chris@17 210 * @return {string}
Chris@17 211 * A view mode ID.
Chris@0 212 */
Chris@17 213 getViewMode() {
Chris@17 214 return this.get('fieldID')
Chris@17 215 .split('/')
Chris@17 216 .pop();
Chris@17 217 },
Chris@0 218
Chris@0 219 /**
Chris@17 220 * Find other instances of this field with different view modes.
Chris@0 221 *
Chris@17 222 * @return {Array}
Chris@17 223 * An array containing view mode IDs.
Chris@0 224 */
Chris@17 225 findOtherViewModes() {
Chris@17 226 const currentField = this;
Chris@17 227 const otherViewModes = [];
Chris@17 228 Drupal.quickedit.collections.fields
Chris@17 229 // Find all instances of fields that display the same logical field
Chris@17 230 // (same entity, same field, just a different instance and maybe a
Chris@17 231 // different view mode).
Chris@17 232 .where({ logicalFieldID: currentField.get('logicalFieldID') })
Chris@17 233 .forEach(field => {
Chris@17 234 // Ignore the current field and other fields with the same view mode.
Chris@17 235 if (
Chris@17 236 field !== currentField &&
Chris@17 237 field.get('fieldID') !== currentField.get('fieldID')
Chris@17 238 ) {
Chris@17 239 otherViewModes.push(field.getViewMode());
Chris@17 240 }
Chris@17 241 });
Chris@17 242 return otherViewModes;
Chris@17 243 },
Chris@17 244 },
Chris@17 245 /** @lends Drupal.quickedit.FieldModel */ {
Chris@17 246 /**
Chris@17 247 * Sequence of all possible states a field can be in during quickediting.
Chris@17 248 *
Chris@17 249 * @type {Array.<string>}
Chris@17 250 */
Chris@17 251 states: [
Chris@17 252 // The field associated with this FieldModel is linked to an EntityModel;
Chris@17 253 // the user can choose to start in-place editing that entity (and
Chris@17 254 // consequently this field). No in-place editor (EditorView) is associated
Chris@17 255 // with this field, because this field is not being in-place edited.
Chris@17 256 // This is both the initial (not yet in-place editing) and the end state
Chris@17 257 // (finished in-place editing).
Chris@17 258 'inactive',
Chris@17 259 // The user is in-place editing this entity, and this field is a
Chris@17 260 // candidate
Chris@17 261 // for in-place editing. In-place editor should not
Chris@17 262 // - Trigger: user.
Chris@17 263 // - Guarantees: entity is ready, in-place editor (EditorView) is
Chris@17 264 // associated with the field.
Chris@17 265 // - Expected behavior: visual indicators
Chris@17 266 // around the field indicate it is available for in-place editing, no
Chris@17 267 // in-place editor presented yet.
Chris@17 268 'candidate',
Chris@17 269 // User is highlighting this field.
Chris@17 270 // - Trigger: user.
Chris@17 271 // - Guarantees: see 'candidate'.
Chris@17 272 // - Expected behavior: visual indicators to convey highlighting, in-place
Chris@17 273 // editing toolbar shows field's label.
Chris@17 274 'highlighted',
Chris@17 275 // User has activated the in-place editing of this field; in-place editor
Chris@17 276 // is activating.
Chris@17 277 // - Trigger: user.
Chris@17 278 // - Guarantees: see 'candidate'.
Chris@17 279 // - Expected behavior: loading indicator, in-place editor is loading
Chris@17 280 // remote data (e.g. retrieve form from back-end). Upon retrieval of
Chris@17 281 // remote data, the in-place editor transitions the field's state to
Chris@17 282 // 'active'.
Chris@17 283 'activating',
Chris@17 284 // In-place editor has finished loading remote data; ready for use.
Chris@17 285 // - Trigger: in-place editor.
Chris@17 286 // - Guarantees: see 'candidate'.
Chris@17 287 // - Expected behavior: in-place editor for the field is ready for use.
Chris@17 288 'active',
Chris@17 289 // User has modified values in the in-place editor.
Chris@17 290 // - Trigger: user.
Chris@17 291 // - Guarantees: see 'candidate', plus in-place editor is ready for use.
Chris@17 292 // - Expected behavior: visual indicator of change.
Chris@17 293 'changed',
Chris@17 294 // User is saving changed field data in in-place editor to
Chris@17 295 // PrivateTempStore. The save mechanism of the in-place editor is called.
Chris@17 296 // - Trigger: user.
Chris@17 297 // - Guarantees: see 'candidate' and 'active'.
Chris@17 298 // - Expected behavior: saving indicator, in-place editor is saving field
Chris@17 299 // data into PrivateTempStore. Upon successful saving (without
Chris@17 300 // validation errors), the in-place editor transitions the field's state
Chris@17 301 // to 'saved', but to 'invalid' upon failed saving (with validation
Chris@17 302 // errors).
Chris@17 303 'saving',
Chris@17 304 // In-place editor has successfully saved the changed field.
Chris@17 305 // - Trigger: in-place editor.
Chris@17 306 // - Guarantees: see 'candidate' and 'active'.
Chris@17 307 // - Expected behavior: transition back to 'candidate' state because the
Chris@17 308 // deed is done. Then: 1) transition to 'inactive' to allow the field
Chris@17 309 // to be rerendered, 2) destroy the FieldModel (which also destroys
Chris@17 310 // attached views like the EditorView), 3) replace the existing field
Chris@17 311 // HTML with the existing HTML and 4) attach behaviors again so that the
Chris@17 312 // field becomes available again for in-place editing.
Chris@17 313 'saved',
Chris@17 314 // In-place editor has failed to saved the changed field: there were
Chris@17 315 // validation errors.
Chris@17 316 // - Trigger: in-place editor.
Chris@17 317 // - Guarantees: see 'candidate' and 'active'.
Chris@17 318 // - Expected behavior: remain in 'invalid' state, let the user make more
Chris@17 319 // changes so that he can save it again, without validation errors.
Chris@17 320 'invalid',
Chris@17 321 ],
Chris@0 322
Chris@0 323 /**
Chris@17 324 * Indicates whether the 'from' state comes before the 'to' state.
Chris@17 325 *
Chris@17 326 * @param {string} from
Chris@17 327 * One of {@link Drupal.quickedit.FieldModel.states}.
Chris@17 328 * @param {string} to
Chris@17 329 * One of {@link Drupal.quickedit.FieldModel.states}.
Chris@17 330 *
Chris@17 331 * @return {bool}
Chris@17 332 * Whether the 'from' state comes before the 'to' state.
Chris@0 333 */
Chris@17 334 followsStateSequence(from, to) {
Chris@17 335 return _.indexOf(this.states, from) < _.indexOf(this.states, to);
Chris@17 336 },
Chris@0 337 },
Chris@17 338 );
Chris@0 339
Chris@0 340 /**
Chris@0 341 * @constructor
Chris@0 342 *
Chris@0 343 * @augments Backbone.Collection
Chris@0 344 */
Chris@17 345 Drupal.quickedit.FieldCollection = Backbone.Collection.extend(
Chris@17 346 /** @lends Drupal.quickedit.FieldCollection */ {
Chris@17 347 /**
Chris@17 348 * @type {Drupal.quickedit.FieldModel}
Chris@17 349 */
Chris@17 350 model: Drupal.quickedit.FieldModel,
Chris@17 351 },
Chris@17 352 );
Chris@17 353 })(_, Backbone, Drupal);