annotate core/modules/quickedit/js/quickedit.es6.js @ 15:e200cb7efeb3

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