annotate core/modules/quickedit/js/quickedit.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 * Attaches behavior for the Quick Edit module.
Chris@0 4 *
Chris@0 5 * Everything happens asynchronously, to allow for:
Chris@0 6 * - dynamically rendered contextual links
Chris@0 7 * - asynchronously retrieved (and cached) per-field in-place editing metadata
Chris@0 8 * - asynchronous setup of in-place editable field and "Quick edit" link.
Chris@0 9 *
Chris@0 10 * To achieve this, there are several queues:
Chris@0 11 * - fieldsMetadataQueue: fields whose metadata still needs to be fetched.
Chris@0 12 * - fieldsAvailableQueue: queue of fields whose metadata is known, and for
Chris@0 13 * which it has been confirmed that the user has permission to edit them.
Chris@0 14 * However, FieldModels will only be created for them once there's a
Chris@0 15 * contextual link for their entity: when it's possible to initiate editing.
Chris@0 16 * - contextualLinksQueue: queue of contextual links on entities for which it
Chris@0 17 * is not yet known whether the user has permission to edit at >=1 of them.
Chris@0 18 */
Chris@0 19
Chris@17 20 (function($, _, Backbone, Drupal, drupalSettings, JSON, storage) {
Chris@17 21 const options = $.extend(
Chris@17 22 drupalSettings.quickedit,
Chris@0 23 // Merge strings on top of drupalSettings so that they are not mutable.
Chris@0 24 {
Chris@0 25 strings: {
Chris@0 26 quickEdit: Drupal.t('Quick edit'),
Chris@0 27 },
Chris@0 28 },
Chris@0 29 );
Chris@0 30
Chris@0 31 /**
Chris@0 32 * Tracks fields without metadata. Contains objects with the following keys:
Chris@0 33 * - DOM el
Chris@0 34 * - String fieldID
Chris@0 35 * - String entityID
Chris@0 36 */
Chris@0 37 let fieldsMetadataQueue = [];
Chris@0 38
Chris@0 39 /**
Chris@0 40 * Tracks fields ready for use. Contains objects with the following keys:
Chris@0 41 * - DOM el
Chris@0 42 * - String fieldID
Chris@0 43 * - String entityID
Chris@0 44 */
Chris@0 45 let fieldsAvailableQueue = [];
Chris@0 46
Chris@0 47 /**
Chris@0 48 * Tracks contextual links on entities. Contains objects with the following
Chris@0 49 * keys:
Chris@0 50 * - String entityID
Chris@0 51 * - DOM el
Chris@0 52 * - DOM region
Chris@0 53 */
Chris@0 54 let contextualLinksQueue = [];
Chris@0 55
Chris@0 56 /**
Chris@0 57 * Tracks how many instances exist for each unique entity. Contains key-value
Chris@0 58 * pairs:
Chris@0 59 * - String entityID
Chris@0 60 * - Number count
Chris@0 61 */
Chris@0 62 const entityInstancesTracker = {};
Chris@0 63
Chris@0 64 /**
Chris@0 65 * Initialize the Quick Edit app.
Chris@0 66 *
Chris@0 67 * @param {HTMLElement} bodyElement
Chris@0 68 * This document's body element.
Chris@0 69 */
Chris@0 70 function initQuickEdit(bodyElement) {
Chris@0 71 Drupal.quickedit.collections.entities = new Drupal.quickedit.EntityCollection();
Chris@0 72 Drupal.quickedit.collections.fields = new Drupal.quickedit.FieldCollection();
Chris@0 73
Chris@0 74 // Instantiate AppModel (application state) and AppView, which is the
Chris@0 75 // controller of the whole in-place editing experience.
Chris@0 76 Drupal.quickedit.app = new Drupal.quickedit.AppView({
Chris@0 77 el: bodyElement,
Chris@0 78 model: new Drupal.quickedit.AppModel(),
Chris@0 79 entitiesCollection: Drupal.quickedit.collections.entities,
Chris@0 80 fieldsCollection: Drupal.quickedit.collections.fields,
Chris@0 81 });
Chris@0 82 }
Chris@0 83
Chris@0 84 /**
Chris@0 85 * Assigns the entity an instance ID.
Chris@0 86 *
Chris@0 87 * @param {HTMLElement} entityElement
Chris@0 88 * A Drupal Entity API entity's DOM element with a data-quickedit-entity-id
Chris@0 89 * attribute.
Chris@0 90 */
Chris@0 91 function processEntity(entityElement) {
Chris@0 92 const entityID = entityElement.getAttribute('data-quickedit-entity-id');
Chris@0 93 if (!entityInstancesTracker.hasOwnProperty(entityID)) {
Chris@0 94 entityInstancesTracker[entityID] = 0;
Chris@17 95 } else {
Chris@0 96 entityInstancesTracker[entityID]++;
Chris@0 97 }
Chris@0 98
Chris@0 99 // Set the calculated entity instance ID for this element.
Chris@0 100 const entityInstanceID = entityInstancesTracker[entityID];
Chris@17 101 entityElement.setAttribute(
Chris@17 102 'data-quickedit-entity-instance-id',
Chris@17 103 entityInstanceID,
Chris@17 104 );
Chris@0 105 }
Chris@0 106
Chris@0 107 /**
Chris@0 108 * Initialize a field; create FieldModel.
Chris@0 109 *
Chris@0 110 * @param {HTMLElement} fieldElement
Chris@0 111 * The field's DOM element.
Chris@0 112 * @param {string} fieldID
Chris@0 113 * The field's ID.
Chris@0 114 * @param {string} entityID
Chris@0 115 * The field's entity's ID.
Chris@0 116 * @param {string} entityInstanceID
Chris@0 117 * The field's entity's instance ID.
Chris@0 118 */
Chris@0 119 function initializeField(fieldElement, fieldID, entityID, entityInstanceID) {
Chris@0 120 const entity = Drupal.quickedit.collections.entities.findWhere({
Chris@0 121 entityID,
Chris@0 122 entityInstanceID,
Chris@0 123 });
Chris@0 124
Chris@0 125 $(fieldElement).addClass('quickedit-field');
Chris@0 126
Chris@0 127 // The FieldModel stores the state of an in-place editable entity field.
Chris@0 128 const field = new Drupal.quickedit.FieldModel({
Chris@0 129 el: fieldElement,
Chris@0 130 fieldID,
Chris@0 131 id: `${fieldID}[${entity.get('entityInstanceID')}]`,
Chris@0 132 entity,
Chris@0 133 metadata: Drupal.quickedit.metadata.get(fieldID),
Chris@17 134 acceptStateChange: _.bind(
Chris@17 135 Drupal.quickedit.app.acceptEditorStateChange,
Chris@17 136 Drupal.quickedit.app,
Chris@17 137 ),
Chris@0 138 });
Chris@0 139
Chris@0 140 // Track all fields on the page.
Chris@0 141 Drupal.quickedit.collections.fields.add(field);
Chris@0 142 }
Chris@0 143
Chris@0 144 /**
Chris@0 145 * Loads missing in-place editor's attachments (JavaScript and CSS files).
Chris@0 146 *
Chris@0 147 * Missing in-place editors are those whose fields are actively being used on
Chris@0 148 * the page but don't have.
Chris@0 149 *
Chris@0 150 * @param {function} callback
Chris@0 151 * Callback function to be called when the missing in-place editors (if any)
Chris@0 152 * have been inserted into the DOM. i.e. they may still be loading.
Chris@0 153 */
Chris@0 154 function loadMissingEditors(callback) {
Chris@0 155 const loadedEditors = _.keys(Drupal.quickedit.editors);
Chris@0 156 let missingEditors = [];
Chris@17 157 Drupal.quickedit.collections.fields.each(fieldModel => {
Chris@0 158 const metadata = Drupal.quickedit.metadata.get(fieldModel.get('fieldID'));
Chris@0 159 if (metadata.access && _.indexOf(loadedEditors, metadata.editor) === -1) {
Chris@0 160 missingEditors.push(metadata.editor);
Chris@0 161 // Set a stub, to prevent subsequent calls to loadMissingEditors() from
Chris@0 162 // loading the same in-place editor again. Loading an in-place editor
Chris@0 163 // requires talking to a server, to download its JavaScript, then
Chris@0 164 // executing its JavaScript, and only then its Drupal.quickedit.editors
Chris@0 165 // entry will be set.
Chris@0 166 Drupal.quickedit.editors[metadata.editor] = false;
Chris@0 167 }
Chris@0 168 });
Chris@0 169 missingEditors = _.uniq(missingEditors);
Chris@0 170 if (missingEditors.length === 0) {
Chris@0 171 callback();
Chris@0 172 return;
Chris@0 173 }
Chris@0 174
Chris@0 175 // @see https://www.drupal.org/node/2029999.
Chris@0 176 // Create a Drupal.Ajax instance to load the form.
Chris@0 177 const loadEditorsAjax = Drupal.ajax({
Chris@0 178 url: Drupal.url('quickedit/attachments'),
Chris@0 179 submit: { 'editors[]': missingEditors },
Chris@0 180 });
Chris@0 181 // Implement a scoped insert AJAX command: calls the callback after all AJAX
Chris@0 182 // command functions have been executed (hence the deferred calling).
Chris@0 183 const realInsert = Drupal.AjaxCommands.prototype.insert;
Chris@17 184 loadEditorsAjax.commands.insert = function(ajax, response, status) {
Chris@0 185 _.defer(callback);
Chris@0 186 realInsert(ajax, response, status);
Chris@0 187 };
Chris@0 188 // Trigger the AJAX request, which will should return AJAX commands to
Chris@0 189 // insert any missing attachments.
Chris@0 190 loadEditorsAjax.execute();
Chris@0 191 }
Chris@0 192
Chris@0 193 /**
Chris@0 194 * Attempts to set up a "Quick edit" link and corresponding EntityModel.
Chris@0 195 *
Chris@0 196 * @param {object} contextualLink
Chris@0 197 * An object with the following properties:
Chris@0 198 * - String entityID: a Quick Edit entity identifier, e.g. "node/1" or
Chris@0 199 * "block_content/5".
Chris@0 200 * - String entityInstanceID: a Quick Edit entity instance identifier,
Chris@0 201 * e.g. 0, 1 or n (depending on whether it's the first, second, or n+1st
Chris@0 202 * instance of this entity).
Chris@0 203 * - DOM el: element pointing to the contextual links placeholder for this
Chris@0 204 * entity.
Chris@0 205 * - DOM region: element pointing to the contextual region of this entity.
Chris@0 206 *
Chris@0 207 * @return {bool}
Chris@0 208 * Returns true when a contextual the given contextual link metadata can be
Chris@0 209 * removed from the queue (either because the contextual link has been set
Chris@0 210 * up or because it is certain that in-place editing is not allowed for any
Chris@0 211 * of its fields). Returns false otherwise.
Chris@0 212 */
Chris@0 213 function initializeEntityContextualLink(contextualLink) {
Chris@0 214 const metadata = Drupal.quickedit.metadata;
Chris@0 215 // Check if the user has permission to edit at least one of them.
Chris@0 216 function hasFieldWithPermission(fieldIDs) {
Chris@0 217 for (let i = 0; i < fieldIDs.length; i++) {
Chris@0 218 const fieldID = fieldIDs[i];
Chris@0 219 if (metadata.get(fieldID, 'access') === true) {
Chris@0 220 return true;
Chris@0 221 }
Chris@0 222 }
Chris@0 223 return false;
Chris@0 224 }
Chris@0 225
Chris@0 226 // Checks if the metadata for all given field IDs exists.
Chris@0 227 function allMetadataExists(fieldIDs) {
Chris@0 228 return fieldIDs.length === metadata.intersection(fieldIDs).length;
Chris@0 229 }
Chris@0 230
Chris@0 231 // Find all fields for this entity instance and collect their field IDs.
Chris@0 232 const fields = _.where(fieldsAvailableQueue, {
Chris@0 233 entityID: contextualLink.entityID,
Chris@0 234 entityInstanceID: contextualLink.entityInstanceID,
Chris@0 235 });
Chris@0 236 const fieldIDs = _.pluck(fields, 'fieldID');
Chris@0 237
Chris@0 238 // No fields found yet.
Chris@0 239 if (fieldIDs.length === 0) {
Chris@0 240 return false;
Chris@0 241 }
Chris@0 242 // The entity for the given contextual link contains at least one field that
Chris@0 243 // the current user may edit in-place; instantiate EntityModel,
Chris@0 244 // EntityDecorationView and ContextualLinkView.
Chris@17 245 if (hasFieldWithPermission(fieldIDs)) {
Chris@0 246 const entityModel = new Drupal.quickedit.EntityModel({
Chris@0 247 el: contextualLink.region,
Chris@0 248 entityID: contextualLink.entityID,
Chris@0 249 entityInstanceID: contextualLink.entityInstanceID,
Chris@0 250 id: `${contextualLink.entityID}[${contextualLink.entityInstanceID}]`,
Chris@0 251 label: Drupal.quickedit.metadata.get(contextualLink.entityID, 'label'),
Chris@0 252 });
Chris@0 253 Drupal.quickedit.collections.entities.add(entityModel);
Chris@0 254 // Create an EntityDecorationView associated with the root DOM node of the
Chris@0 255 // entity.
Chris@0 256 const entityDecorationView = new Drupal.quickedit.EntityDecorationView({
Chris@0 257 el: contextualLink.region,
Chris@0 258 model: entityModel,
Chris@0 259 });
Chris@0 260 entityModel.set('entityDecorationView', entityDecorationView);
Chris@0 261
Chris@0 262 // Initialize all queued fields within this entity (creates FieldModels).
Chris@17 263 _.each(fields, field => {
Chris@17 264 initializeField(
Chris@17 265 field.el,
Chris@17 266 field.fieldID,
Chris@17 267 contextualLink.entityID,
Chris@17 268 contextualLink.entityInstanceID,
Chris@17 269 );
Chris@0 270 });
Chris@0 271 fieldsAvailableQueue = _.difference(fieldsAvailableQueue, fields);
Chris@0 272
Chris@0 273 // Initialization should only be called once. Use Underscore's once method
Chris@0 274 // to get a one-time use version of the function.
Chris@0 275 const initContextualLink = _.once(() => {
Chris@0 276 const $links = $(contextualLink.el).find('.contextual-links');
Chris@17 277 const contextualLinkView = new Drupal.quickedit.ContextualLinkView(
Chris@17 278 $.extend(
Chris@17 279 {
Chris@17 280 el: $(
Chris@17 281 '<li class="quickedit"><a href="" role="button" aria-pressed="false"></a></li>',
Chris@17 282 ).prependTo($links),
Chris@17 283 model: entityModel,
Chris@17 284 appModel: Drupal.quickedit.app.model,
Chris@17 285 },
Chris@17 286 options,
Chris@17 287 ),
Chris@17 288 );
Chris@0 289 entityModel.set('contextualLinkView', contextualLinkView);
Chris@0 290 });
Chris@0 291
Chris@0 292 // Set up ContextualLinkView after loading any missing in-place editors.
Chris@0 293 loadMissingEditors(initContextualLink);
Chris@0 294
Chris@0 295 return true;
Chris@0 296 }
Chris@0 297 // There was not at least one field that the current user may edit in-place,
Chris@0 298 // even though the metadata for all fields within this entity is available.
Chris@17 299 if (allMetadataExists(fieldIDs)) {
Chris@0 300 return true;
Chris@0 301 }
Chris@0 302
Chris@0 303 return false;
Chris@0 304 }
Chris@0 305
Chris@0 306 /**
Chris@17 307 * Extracts the entity ID from a field ID.
Chris@17 308 *
Chris@17 309 * @param {string} fieldID
Chris@17 310 * A field ID: a string of the format
Chris@17 311 * `<entity type>/<id>/<field name>/<language>/<view mode>`.
Chris@17 312 *
Chris@17 313 * @return {string}
Chris@17 314 * An entity ID: a string of the format `<entity type>/<id>`.
Chris@17 315 */
Chris@17 316 function extractEntityID(fieldID) {
Chris@17 317 return fieldID
Chris@17 318 .split('/')
Chris@17 319 .slice(0, 2)
Chris@17 320 .join('/');
Chris@17 321 }
Chris@17 322
Chris@17 323 /**
Chris@17 324 * Fetch the field's metadata; queue or initialize it (if EntityModel exists).
Chris@17 325 *
Chris@17 326 * @param {HTMLElement} fieldElement
Chris@17 327 * A Drupal Field API field's DOM element with a data-quickedit-field-id
Chris@17 328 * attribute.
Chris@17 329 */
Chris@17 330 function processField(fieldElement) {
Chris@17 331 const metadata = Drupal.quickedit.metadata;
Chris@17 332 const fieldID = fieldElement.getAttribute('data-quickedit-field-id');
Chris@17 333 const entityID = extractEntityID(fieldID);
Chris@17 334 // Figure out the instance ID by looking at the ancestor
Chris@17 335 // [data-quickedit-entity-id] element's data-quickedit-entity-instance-id
Chris@17 336 // attribute.
Chris@17 337 const entityElementSelector = `[data-quickedit-entity-id="${entityID}"]`;
Chris@17 338 const $entityElement = $(entityElementSelector);
Chris@17 339
Chris@17 340 // If there are no elements returned from `entityElementSelector`
Chris@17 341 // throw an error. Check the browser console for this message.
Chris@17 342 if (!$entityElement.length) {
Chris@17 343 throw new Error(
Chris@17 344 `Quick Edit could not associate the rendered entity field markup (with [data-quickedit-field-id="${fieldID}"]) with the corresponding rendered entity markup: no parent DOM node found with [data-quickedit-entity-id="${entityID}"]. This is typically caused by the theme's template for this entity type forgetting to print the attributes.`,
Chris@17 345 );
Chris@17 346 }
Chris@17 347 let entityElement = $(fieldElement).closest($entityElement);
Chris@17 348
Chris@17 349 // In the case of a full entity view page, the entity title is rendered
Chris@17 350 // outside of "the entity DOM node": it's rendered as the page title. So in
Chris@17 351 // this case, we find the lowest common parent element (deepest in the tree)
Chris@17 352 // and consider that the entity element.
Chris@17 353 if (entityElement.length === 0) {
Chris@17 354 const $lowestCommonParent = $entityElement
Chris@17 355 .parents()
Chris@17 356 .has(fieldElement)
Chris@17 357 .first();
Chris@17 358 entityElement = $lowestCommonParent.find($entityElement);
Chris@17 359 }
Chris@17 360 const entityInstanceID = entityElement
Chris@17 361 .get(0)
Chris@17 362 .getAttribute('data-quickedit-entity-instance-id');
Chris@17 363
Chris@17 364 // Early-return if metadata for this field is missing.
Chris@17 365 if (!metadata.has(fieldID)) {
Chris@17 366 fieldsMetadataQueue.push({
Chris@17 367 el: fieldElement,
Chris@17 368 fieldID,
Chris@17 369 entityID,
Chris@17 370 entityInstanceID,
Chris@17 371 });
Chris@17 372 return;
Chris@17 373 }
Chris@17 374 // Early-return if the user is not allowed to in-place edit this field.
Chris@17 375 if (metadata.get(fieldID, 'access') !== true) {
Chris@17 376 return;
Chris@17 377 }
Chris@17 378
Chris@17 379 // If an EntityModel for this field already exists (and hence also a "Quick
Chris@17 380 // edit" contextual link), then initialize it immediately.
Chris@17 381 if (
Chris@17 382 Drupal.quickedit.collections.entities.findWhere({
Chris@17 383 entityID,
Chris@17 384 entityInstanceID,
Chris@17 385 })
Chris@17 386 ) {
Chris@17 387 initializeField(fieldElement, fieldID, entityID, entityInstanceID);
Chris@17 388 }
Chris@17 389 // Otherwise: queue the field. It is now available to be set up when its
Chris@17 390 // corresponding entity becomes in-place editable.
Chris@17 391 else {
Chris@17 392 fieldsAvailableQueue.push({
Chris@17 393 el: fieldElement,
Chris@17 394 fieldID,
Chris@17 395 entityID,
Chris@17 396 entityInstanceID,
Chris@17 397 });
Chris@17 398 }
Chris@17 399 }
Chris@17 400
Chris@17 401 /**
Chris@0 402 * Delete models and queue items that are contained within a given context.
Chris@0 403 *
Chris@0 404 * Deletes any contained EntityModels (plus their associated FieldModels and
Chris@0 405 * ContextualLinkView) and FieldModels, as well as the corresponding queues.
Chris@0 406 *
Chris@0 407 * After EntityModels, FieldModels must also be deleted, because it is
Chris@0 408 * possible in Drupal for a field DOM element to exist outside of the entity
Chris@0 409 * DOM element, e.g. when viewing the full node, the title of the node is not
Chris@0 410 * rendered within the node (the entity) but as the page title.
Chris@0 411 *
Chris@0 412 * Note: this will not delete an entity that is actively being in-place
Chris@0 413 * edited.
Chris@0 414 *
Chris@0 415 * @param {jQuery} $context
Chris@0 416 * The context within which to delete.
Chris@0 417 */
Chris@0 418 function deleteContainedModelsAndQueues($context) {
Chris@17 419 $context
Chris@17 420 .find('[data-quickedit-entity-id]')
Chris@17 421 .addBack('[data-quickedit-entity-id]')
Chris@17 422 .each((index, entityElement) => {
Chris@17 423 // Delete entity model.
Chris@17 424 const entityModel = Drupal.quickedit.collections.entities.findWhere({
Chris@17 425 el: entityElement,
Chris@17 426 });
Chris@17 427 if (entityModel) {
Chris@17 428 const contextualLinkView = entityModel.get('contextualLinkView');
Chris@17 429 contextualLinkView.undelegateEvents();
Chris@17 430 contextualLinkView.remove();
Chris@17 431 // Remove the EntityDecorationView.
Chris@17 432 entityModel.get('entityDecorationView').remove();
Chris@17 433 // Destroy the EntityModel; this will also destroy its FieldModels.
Chris@17 434 entityModel.destroy();
Chris@17 435 }
Chris@17 436
Chris@17 437 // Filter queue.
Chris@17 438 function hasOtherRegion(contextualLink) {
Chris@17 439 return contextualLink.region !== entityElement;
Chris@17 440 }
Chris@17 441
Chris@17 442 contextualLinksQueue = _.filter(contextualLinksQueue, hasOtherRegion);
Chris@17 443 });
Chris@17 444
Chris@17 445 $context
Chris@17 446 .find('[data-quickedit-field-id]')
Chris@17 447 .addBack('[data-quickedit-field-id]')
Chris@17 448 .each((index, fieldElement) => {
Chris@17 449 // Delete field models.
Chris@17 450 Drupal.quickedit.collections.fields
Chris@17 451 .chain()
Chris@17 452 .filter(fieldModel => fieldModel.get('el') === fieldElement)
Chris@17 453 .invoke('destroy');
Chris@17 454
Chris@17 455 // Filter queues.
Chris@17 456 function hasOtherFieldElement(field) {
Chris@17 457 return field.el !== fieldElement;
Chris@17 458 }
Chris@17 459
Chris@17 460 fieldsMetadataQueue = _.filter(
Chris@17 461 fieldsMetadataQueue,
Chris@17 462 hasOtherFieldElement,
Chris@17 463 );
Chris@17 464 fieldsAvailableQueue = _.filter(
Chris@17 465 fieldsAvailableQueue,
Chris@17 466 hasOtherFieldElement,
Chris@17 467 );
Chris@17 468 });
Chris@17 469 }
Chris@17 470
Chris@17 471 /**
Chris@17 472 * Fetches metadata for fields whose metadata is missing.
Chris@17 473 *
Chris@17 474 * Fields whose metadata is missing are tracked at fieldsMetadataQueue.
Chris@17 475 *
Chris@17 476 * @param {function} callback
Chris@17 477 * A callback function that receives field elements whose metadata will just
Chris@17 478 * have been fetched.
Chris@17 479 */
Chris@17 480 function fetchMissingMetadata(callback) {
Chris@17 481 if (fieldsMetadataQueue.length) {
Chris@17 482 const fieldIDs = _.pluck(fieldsMetadataQueue, 'fieldID');
Chris@17 483 const fieldElementsWithoutMetadata = _.pluck(fieldsMetadataQueue, 'el');
Chris@17 484 let entityIDs = _.uniq(_.pluck(fieldsMetadataQueue, 'entityID'), true);
Chris@17 485 // Ensure we only request entityIDs for which we don't have metadata yet.
Chris@17 486 entityIDs = _.difference(
Chris@17 487 entityIDs,
Chris@17 488 Drupal.quickedit.metadata.intersection(entityIDs),
Chris@17 489 );
Chris@17 490 fieldsMetadataQueue = [];
Chris@17 491
Chris@17 492 $.ajax({
Chris@17 493 url: Drupal.url('quickedit/metadata'),
Chris@17 494 type: 'POST',
Chris@17 495 data: {
Chris@17 496 'fields[]': fieldIDs,
Chris@17 497 'entities[]': entityIDs,
Chris@17 498 },
Chris@17 499 dataType: 'json',
Chris@17 500 success(results) {
Chris@17 501 // Store the metadata.
Chris@17 502 _.each(results, (fieldMetadata, fieldID) => {
Chris@17 503 Drupal.quickedit.metadata.add(fieldID, fieldMetadata);
Chris@17 504 });
Chris@17 505
Chris@17 506 callback(fieldElementsWithoutMetadata);
Chris@17 507 },
Chris@17 508 });
Chris@17 509 }
Chris@17 510 }
Chris@17 511
Chris@17 512 /**
Chris@17 513 *
Chris@17 514 * @type {Drupal~behavior}
Chris@17 515 */
Chris@17 516 Drupal.behaviors.quickedit = {
Chris@17 517 attach(context) {
Chris@17 518 // Initialize the Quick Edit app once per page load.
Chris@17 519 $('body')
Chris@17 520 .once('quickedit-init')
Chris@17 521 .each(initQuickEdit);
Chris@17 522
Chris@17 523 // Find all in-place editable fields, if any.
Chris@17 524 const $fields = $(context)
Chris@17 525 .find('[data-quickedit-field-id]')
Chris@17 526 .once('quickedit');
Chris@17 527 if ($fields.length === 0) {
Chris@17 528 return;
Chris@0 529 }
Chris@0 530
Chris@17 531 // Process each entity element: identical entities that appear multiple
Chris@17 532 // times will get a numeric identifier, starting at 0.
Chris@17 533 $(context)
Chris@17 534 .find('[data-quickedit-entity-id]')
Chris@17 535 .once('quickedit')
Chris@17 536 .each((index, entityElement) => {
Chris@17 537 processEntity(entityElement);
Chris@17 538 });
Chris@17 539
Chris@17 540 // Process each field element: queue to be used or to fetch metadata.
Chris@17 541 // When a field is being rerendered after editing, it will be processed
Chris@17 542 // immediately. New fields will be unable to be processed immediately,
Chris@17 543 // but will instead be queued to have their metadata fetched, which occurs
Chris@17 544 // below in fetchMissingMetaData().
Chris@17 545 $fields.each((index, fieldElement) => {
Chris@17 546 processField(fieldElement);
Chris@17 547 });
Chris@17 548
Chris@17 549 // Entities and fields on the page have been detected, try to set up the
Chris@17 550 // contextual links for those entities that already have the necessary
Chris@17 551 // meta- data in the client-side cache.
Chris@17 552 contextualLinksQueue = _.filter(
Chris@17 553 contextualLinksQueue,
Chris@17 554 contextualLink => !initializeEntityContextualLink(contextualLink),
Chris@17 555 );
Chris@17 556
Chris@17 557 // Fetch metadata for any fields that are queued to retrieve it.
Chris@17 558 fetchMissingMetadata(fieldElementsWithFreshMetadata => {
Chris@17 559 // Metadata has been fetched, reprocess fields whose metadata was
Chris@17 560 // missing.
Chris@17 561 _.each(fieldElementsWithFreshMetadata, processField);
Chris@17 562
Chris@17 563 // Metadata has been fetched, try to set up more contextual links now.
Chris@17 564 contextualLinksQueue = _.filter(
Chris@17 565 contextualLinksQueue,
Chris@17 566 contextualLink => !initializeEntityContextualLink(contextualLink),
Chris@17 567 );
Chris@17 568 });
Chris@17 569 },
Chris@17 570 detach(context, settings, trigger) {
Chris@17 571 if (trigger === 'unload') {
Chris@17 572 deleteContainedModelsAndQueues($(context));
Chris@0 573 }
Chris@17 574 },
Chris@17 575 };
Chris@0 576
Chris@17 577 /**
Chris@17 578 *
Chris@17 579 * @namespace
Chris@17 580 */
Chris@17 581 Drupal.quickedit = {
Chris@17 582 /**
Chris@17 583 * A {@link Drupal.quickedit.AppView} instance.
Chris@17 584 */
Chris@17 585 app: null,
Chris@0 586
Chris@17 587 /**
Chris@17 588 * @type {object}
Chris@17 589 *
Chris@17 590 * @prop {Array.<Drupal.quickedit.EntityModel>} entities
Chris@17 591 * @prop {Array.<Drupal.quickedit.FieldModel>} fields
Chris@17 592 */
Chris@17 593 collections: {
Chris@17 594 // All in-place editable entities (Drupal.quickedit.EntityModel) on the
Chris@17 595 // page.
Chris@17 596 entities: null,
Chris@17 597 // All in-place editable fields (Drupal.quickedit.FieldModel) on the page.
Chris@17 598 fields: null,
Chris@17 599 },
Chris@0 600
Chris@17 601 /**
Chris@17 602 * In-place editors will register themselves in this object.
Chris@17 603 *
Chris@17 604 * @namespace
Chris@17 605 */
Chris@17 606 editors: {},
Chris@17 607
Chris@17 608 /**
Chris@17 609 * Per-field metadata that indicates whether in-place editing is allowed,
Chris@17 610 * which in-place editor should be used, etc.
Chris@17 611 *
Chris@17 612 * @namespace
Chris@17 613 */
Chris@17 614 metadata: {
Chris@17 615 /**
Chris@17 616 * Check if a field exists in storage.
Chris@17 617 *
Chris@17 618 * @param {string} fieldID
Chris@17 619 * The field id to check.
Chris@17 620 *
Chris@17 621 * @return {bool}
Chris@17 622 * Whether it was found or not.
Chris@17 623 */
Chris@17 624 has(fieldID) {
Chris@17 625 return storage.getItem(this._prefixFieldID(fieldID)) !== null;
Chris@17 626 },
Chris@17 627
Chris@17 628 /**
Chris@17 629 * Add metadata to a field id.
Chris@17 630 *
Chris@17 631 * @param {string} fieldID
Chris@17 632 * The field ID to add data to.
Chris@17 633 * @param {object} metadata
Chris@17 634 * Metadata to add.
Chris@17 635 */
Chris@17 636 add(fieldID, metadata) {
Chris@17 637 storage.setItem(this._prefixFieldID(fieldID), JSON.stringify(metadata));
Chris@17 638 },
Chris@17 639
Chris@17 640 /**
Chris@17 641 * Get a key from a field id.
Chris@17 642 *
Chris@17 643 * @param {string} fieldID
Chris@17 644 * The field ID to check.
Chris@17 645 * @param {string} [key]
Chris@17 646 * The key to check. If empty, will return all metadata.
Chris@17 647 *
Chris@17 648 * @return {object|*}
Chris@17 649 * The value for the key, if defined. Otherwise will return all metadata
Chris@17 650 * for the specified field id.
Chris@17 651 *
Chris@17 652 */
Chris@17 653 get(fieldID, key) {
Chris@17 654 const metadata = JSON.parse(
Chris@17 655 storage.getItem(this._prefixFieldID(fieldID)),
Chris@17 656 );
Chris@17 657 return typeof key === 'undefined' ? metadata : metadata[key];
Chris@17 658 },
Chris@17 659
Chris@17 660 /**
Chris@17 661 * Prefix the field id.
Chris@17 662 *
Chris@17 663 * @param {string} fieldID
Chris@17 664 * The field id to prefix.
Chris@17 665 *
Chris@17 666 * @return {string}
Chris@17 667 * A prefixed field id.
Chris@17 668 */
Chris@17 669 _prefixFieldID(fieldID) {
Chris@17 670 return `Drupal.quickedit.metadata.${fieldID}`;
Chris@17 671 },
Chris@17 672
Chris@17 673 /**
Chris@17 674 * Unprefix the field id.
Chris@17 675 *
Chris@17 676 * @param {string} fieldID
Chris@17 677 * The field id to unprefix.
Chris@17 678 *
Chris@17 679 * @return {string}
Chris@17 680 * An unprefixed field id.
Chris@17 681 */
Chris@17 682 _unprefixFieldID(fieldID) {
Chris@17 683 // Strip "Drupal.quickedit.metadata.", which is 26 characters long.
Chris@17 684 return fieldID.substring(26);
Chris@17 685 },
Chris@17 686
Chris@17 687 /**
Chris@17 688 * Intersection calculation.
Chris@17 689 *
Chris@17 690 * @param {Array} fieldIDs
Chris@17 691 * An array of field ids to compare to prefix field id.
Chris@17 692 *
Chris@17 693 * @return {Array}
Chris@17 694 * The intersection found.
Chris@17 695 */
Chris@17 696 intersection(fieldIDs) {
Chris@17 697 const prefixedFieldIDs = _.map(fieldIDs, this._prefixFieldID);
Chris@17 698 const intersection = _.intersection(
Chris@17 699 prefixedFieldIDs,
Chris@17 700 _.keys(sessionStorage),
Chris@17 701 );
Chris@17 702 return _.map(intersection, this._unprefixFieldID);
Chris@17 703 },
Chris@17 704 },
Chris@17 705 };
Chris@17 706
Chris@17 707 // Clear the Quick Edit metadata cache whenever the current user's set of
Chris@17 708 // permissions changes.
Chris@17 709 const permissionsHashKey = Drupal.quickedit.metadata._prefixFieldID(
Chris@17 710 'permissionsHash',
Chris@17 711 );
Chris@17 712 const permissionsHashValue = storage.getItem(permissionsHashKey);
Chris@17 713 const permissionsHash = drupalSettings.user.permissionsHash;
Chris@17 714 if (permissionsHashValue !== permissionsHash) {
Chris@17 715 if (typeof permissionsHash === 'string') {
Chris@17 716 _.chain(storage)
Chris@17 717 .keys()
Chris@17 718 .each(key => {
Chris@17 719 if (key.substring(0, 26) === 'Drupal.quickedit.metadata.') {
Chris@17 720 storage.removeItem(key);
Chris@17 721 }
Chris@17 722 });
Chris@17 723 }
Chris@17 724 storage.setItem(permissionsHashKey, permissionsHash);
Chris@17 725 }
Chris@17 726
Chris@17 727 /**
Chris@17 728 * Detect contextual links on entities annotated by quickedit.
Chris@17 729 *
Chris@17 730 * Queue contextual links to be processed.
Chris@17 731 *
Chris@17 732 * @param {jQuery.Event} event
Chris@17 733 * The `drupalContextualLinkAdded` event.
Chris@17 734 * @param {object} data
Chris@17 735 * An object containing the data relevant to the event.
Chris@17 736 *
Chris@17 737 * @listens event:drupalContextualLinkAdded
Chris@17 738 */
Chris@17 739 $(document).on('drupalContextualLinkAdded', (event, data) => {
Chris@17 740 if (data.$region.is('[data-quickedit-entity-id]')) {
Chris@17 741 // If the contextual link is cached on the client side, an entity instance
Chris@17 742 // will not yet have been assigned. So assign one.
Chris@17 743 if (!data.$region.is('[data-quickedit-entity-instance-id]')) {
Chris@17 744 data.$region.once('quickedit');
Chris@17 745 processEntity(data.$region.get(0));
Chris@0 746 }
Chris@17 747 const contextualLink = {
Chris@17 748 entityID: data.$region.attr('data-quickedit-entity-id'),
Chris@17 749 entityInstanceID: data.$region.attr(
Chris@17 750 'data-quickedit-entity-instance-id',
Chris@17 751 ),
Chris@17 752 el: data.$el[0],
Chris@17 753 region: data.$region[0],
Chris@17 754 };
Chris@17 755 // Set up contextual links for this, otherwise queue it to be set up
Chris@17 756 // later.
Chris@17 757 if (!initializeEntityContextualLink(contextualLink)) {
Chris@17 758 contextualLinksQueue.push(contextualLink);
Chris@17 759 }
Chris@17 760 }
Chris@17 761 });
Chris@17 762 })(
Chris@17 763 jQuery,
Chris@17 764 _,
Chris@17 765 Backbone,
Chris@17 766 Drupal,
Chris@17 767 drupalSettings,
Chris@17 768 window.JSON,
Chris@17 769 window.sessionStorage,
Chris@17 770 );