Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / contextual / js / contextual.es6.js
1 /**
2  * @file
3  * Attaches behaviors for the Contextual module.
4  */
5
6 (function ($, Drupal, drupalSettings, _, Backbone, JSON, storage) {
7   const options = $.extend(drupalSettings.contextual,
8     // Merge strings on top of drupalSettings so that they are not mutable.
9     {
10       strings: {
11         open: Drupal.t('Open'),
12         close: Drupal.t('Close'),
13       },
14     },
15   );
16
17   // Clear the cached contextual links whenever the current user's set of
18   // permissions changes.
19   const cachedPermissionsHash = storage.getItem('Drupal.contextual.permissionsHash');
20   const permissionsHash = drupalSettings.user.permissionsHash;
21   if (cachedPermissionsHash !== permissionsHash) {
22     if (typeof permissionsHash === 'string') {
23       _.chain(storage).keys().each((key) => {
24         if (key.substring(0, 18) === 'Drupal.contextual.') {
25           storage.removeItem(key);
26         }
27       });
28     }
29     storage.setItem('Drupal.contextual.permissionsHash', permissionsHash);
30   }
31
32   /**
33    * Initializes a contextual link: updates its DOM, sets up model and views.
34    *
35    * @param {jQuery} $contextual
36    *   A contextual links placeholder DOM element, containing the actual
37    *   contextual links as rendered by the server.
38    * @param {string} html
39    *   The server-side rendered HTML for this contextual link.
40    */
41   function initContextual($contextual, html) {
42     const $region = $contextual.closest('.contextual-region');
43     const contextual = Drupal.contextual;
44
45     $contextual
46       // Update the placeholder to contain its rendered contextual links.
47       .html(html)
48       // Use the placeholder as a wrapper with a specific class to provide
49       // positioning and behavior attachment context.
50       .addClass('contextual')
51       // Ensure a trigger element exists before the actual contextual links.
52       .prepend(Drupal.theme('contextualTrigger'));
53
54     // Set the destination parameter on each of the contextual links.
55     const destination = `destination=${Drupal.encodePath(drupalSettings.path.currentPath)}`;
56     $contextual.find('.contextual-links a').each(function () {
57       const url = this.getAttribute('href');
58       const glue = (url.indexOf('?') === -1) ? '?' : '&';
59       this.setAttribute('href', url + glue + destination);
60     });
61
62     // Create a model and the appropriate views.
63     const model = new contextual.StateModel({
64       title: $region.find('h2').eq(0).text().trim(),
65     });
66     const viewOptions = $.extend({ el: $contextual, model }, options);
67     contextual.views.push({
68       visual: new contextual.VisualView(viewOptions),
69       aural: new contextual.AuralView(viewOptions),
70       keyboard: new contextual.KeyboardView(viewOptions),
71     });
72     contextual.regionViews.push(new contextual.RegionView(
73       $.extend({ el: $region, model }, options)),
74     );
75
76     // Add the model to the collection. This must happen after the views have
77     // been associated with it, otherwise collection change event handlers can't
78     // trigger the model change event handler in its views.
79     contextual.collection.add(model);
80
81     // Let other JavaScript react to the adding of a new contextual link.
82     $(document).trigger('drupalContextualLinkAdded', {
83       $el: $contextual,
84       $region,
85       model,
86     });
87
88     // Fix visual collisions between contextual link triggers.
89     adjustIfNestedAndOverlapping($contextual);
90   }
91
92   /**
93    * Determines if a contextual link is nested & overlapping, if so: adjusts it.
94    *
95    * This only deals with two levels of nesting; deeper levels are not touched.
96    *
97    * @param {jQuery} $contextual
98    *   A contextual links placeholder DOM element, containing the actual
99    *   contextual links as rendered by the server.
100    */
101   function adjustIfNestedAndOverlapping($contextual) {
102     const $contextuals = $contextual
103       // @todo confirm that .closest() is not sufficient
104       .parents('.contextual-region').eq(-1)
105       .find('.contextual');
106
107     // Early-return when there's no nesting.
108     if ($contextuals.length <= 1) {
109       return;
110     }
111
112     // If the two contextual links overlap, then we move the second one.
113     const firstTop = $contextuals.eq(0).offset().top;
114     const secondTop = $contextuals.eq(1).offset().top;
115     if (firstTop === secondTop) {
116       const $nestedContextual = $contextuals.eq(1);
117
118       // Retrieve height of nested contextual link.
119       let height = 0;
120       const $trigger = $nestedContextual.find('.trigger');
121       // Elements with the .visually-hidden class have no dimensions, so this
122       // class must be temporarily removed to the calculate the height.
123       $trigger.removeClass('visually-hidden');
124       height = $nestedContextual.height();
125       $trigger.addClass('visually-hidden');
126
127       // Adjust nested contextual link's position.
128       $nestedContextual.css({ top: $nestedContextual.position().top + height });
129     }
130   }
131
132   /**
133    * Attaches outline behavior for regions associated with contextual links.
134    *
135    * Events
136    *   Contextual triggers an event that can be used by other scripts.
137    *   - drupalContextualLinkAdded: Triggered when a contextual link is added.
138    *
139    * @type {Drupal~behavior}
140    *
141    * @prop {Drupal~behaviorAttach} attach
142    *  Attaches the outline behavior to the right context.
143    */
144   Drupal.behaviors.contextual = {
145     attach(context) {
146       const $context = $(context);
147
148       // Find all contextual links placeholders, if any.
149       let $placeholders = $context.find('[data-contextual-id]').once('contextual-render');
150       if ($placeholders.length === 0) {
151         return;
152       }
153
154       // Collect the IDs for all contextual links placeholders.
155       const ids = [];
156       $placeholders.each(function () {
157         ids.push($(this).attr('data-contextual-id'));
158       });
159
160       // Update all contextual links placeholders whose HTML is cached.
161       const uncachedIDs = _.filter(ids, (contextualID) => {
162         const html = storage.getItem(`Drupal.contextual.${contextualID}`);
163         if (html && html.length) {
164           // Initialize after the current execution cycle, to make the AJAX
165           // request for retrieving the uncached contextual links as soon as
166           // possible, but also to ensure that other Drupal behaviors have had
167           // the chance to set up an event listener on the Backbone collection
168           // Drupal.contextual.collection.
169           window.setTimeout(() => {
170             initContextual($context.find(`[data-contextual-id="${contextualID}"]`), html);
171           });
172           return false;
173         }
174         return true;
175       });
176
177       // Perform an AJAX request to let the server render the contextual links
178       // for each of the placeholders.
179       if (uncachedIDs.length > 0) {
180         $.ajax({
181           url: Drupal.url('contextual/render'),
182           type: 'POST',
183           data: { 'ids[]': uncachedIDs },
184           dataType: 'json',
185           success(results) {
186             _.each(results, (html, contextualID) => {
187               // Store the metadata.
188               storage.setItem(`Drupal.contextual.${contextualID}`, html);
189               // If the rendered contextual links are empty, then the current
190               // user does not have permission to access the associated links:
191               // don't render anything.
192               if (html.length > 0) {
193                 // Update the placeholders to contain its rendered contextual
194                 // links. Usually there will only be one placeholder, but it's
195                 // possible for multiple identical placeholders exist on the
196                 // page (probably because the same content appears more than
197                 // once).
198                 $placeholders = $context.find(`[data-contextual-id="${contextualID}"]`);
199
200                 // Initialize the contextual links.
201                 for (let i = 0; i < $placeholders.length; i++) {
202                   initContextual($placeholders.eq(i), html);
203                 }
204               }
205             });
206           },
207         });
208       }
209     },
210   };
211
212   /**
213    * Namespace for contextual related functionality.
214    *
215    * @namespace
216    */
217   Drupal.contextual = {
218
219     /**
220      * The {@link Drupal.contextual.View} instances associated with each list
221      * element of contextual links.
222      *
223      * @type {Array}
224      */
225     views: [],
226
227     /**
228      * The {@link Drupal.contextual.RegionView} instances associated with each
229      * contextual region element.
230      *
231      * @type {Array}
232      */
233     regionViews: [],
234   };
235
236   /**
237    * A Backbone.Collection of {@link Drupal.contextual.StateModel} instances.
238    *
239    * @type {Backbone.Collection}
240    */
241   Drupal.contextual.collection = new Backbone.Collection([], { model: Drupal.contextual.StateModel });
242
243   /**
244    * A trigger is an interactive element often bound to a click handler.
245    *
246    * @return {string}
247    *   A string representing a DOM fragment.
248    */
249   Drupal.theme.contextualTrigger = function () {
250     return '<button class="trigger visually-hidden focusable" type="button"></button>';
251   };
252 }(jQuery, Drupal, drupalSettings, _, Backbone, window.JSON, window.sessionStorage));