3 * Attaches behaviors for the Contextual module.
6 (function ($, Drupal, drupalSettings, _, Backbone, JSON, storage) {
10 var options = $.extend(drupalSettings.contextual,
11 // Merge strings on top of drupalSettings so that they are not mutable.
14 open: Drupal.t('Open'),
15 close: Drupal.t('Close')
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);
32 storage.setItem('Drupal.contextual.permissionsHash', permissionsHash);
36 * Initializes a contextual link: updates its DOM, sets up model and views.
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.
44 function initContextual($contextual, html) {
45 var $region = $contextual.closest('.contextual-region');
46 var contextual = Drupal.contextual;
49 // Update the placeholder to contain its rendered contextual links.
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'));
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);
65 // Create a model and the appropriate views.
66 var model = new contextual.StateModel({
67 title: $region.find('h2').eq(0).text().trim()
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)
75 contextual.regionViews.push(new contextual.RegionView(
76 $.extend({el: $region, model: model}, options))
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);
84 // Let other JavaScript react to the adding of a new contextual link.
85 $(document).trigger('drupalContextualLinkAdded', {
91 // Fix visual collisions between contextual link triggers.
92 adjustIfNestedAndOverlapping($contextual);
96 * Determines if a contextual link is nested & overlapping, if so: adjusts it.
98 * This only deals with two levels of nesting; deeper levels are not touched.
100 * @param {jQuery} $contextual
101 * A contextual links placeholder DOM element, containing the actual
102 * contextual links as rendered by the server.
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');
110 // Early-return when there's no nesting.
111 if ($contextuals.length === 1) {
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);
121 // Retrieve height of nested contextual link.
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');
130 // Adjust nested contextual link's position.
131 $nestedContextual.css({top: $nestedContextual.position().top + height});
136 * Attaches outline behavior for regions associated with contextual links.
139 * Contextual triggers an event that can be used by other scripts.
140 * - drupalContextualLinkAdded: Triggered when a contextual link is added.
142 * @type {Drupal~behavior}
144 * @prop {Drupal~behaviorAttach} attach
145 * Attaches the outline behavior to the right context.
147 Drupal.behaviors.contextual = {
148 attach: function (context) {
149 var $context = $(context);
151 // Find all contextual links placeholders, if any.
152 var $placeholders = $context.find('[data-contextual-id]').once('contextual-render');
153 if ($placeholders.length === 0) {
157 // Collect the IDs for all contextual links placeholders.
159 $placeholders.each(function () {
160 ids.push($(this).attr('data-contextual-id'));
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);
180 // Perform an AJAX request to let the server render the contextual links
181 // for each of the placeholders.
182 if (uncachedIDs.length > 0) {
184 url: Drupal.url('contextual/render'),
186 data: {'ids[]': uncachedIDs},
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
201 $placeholders = $context.find('[data-contextual-id="' + contextualID + '"]');
203 // Initialize the contextual links.
204 for (var i = 0; i < $placeholders.length; i++) {
205 initContextual($placeholders.eq(i), html);
216 * Namespace for contextual related functionality.
220 Drupal.contextual = {
223 * The {@link Drupal.contextual.View} instances associated with each list
224 * element of contextual links.
231 * The {@link Drupal.contextual.RegionView} instances associated with each
232 * contextual region element.
240 * A Backbone.Collection of {@link Drupal.contextual.StateModel} instances.
242 * @type {Backbone.Collection}
244 Drupal.contextual.collection = new Backbone.Collection([], {model: Drupal.contextual.StateModel});
247 * A trigger is an interactive element often bound to a click handler.
250 * A string representing a DOM fragment.
252 Drupal.theme.contextualTrigger = function () {
253 return '<button class="trigger visually-hidden focusable" type="button"></button>';
256 })(jQuery, Drupal, drupalSettings, _, Backbone, window.JSON, window.sessionStorage);