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