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