contextual.es6.js 9.2 KB

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