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