contextual.js 8.9 KB

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