contextual.es6.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. /**
  2. * @file
  3. * Attaches behaviors for the Contextual module.
  4. */
  5. (function($, Drupal, drupalSettings, _, Backbone, JSON, storage) {
  6. const options = $.extend(
  7. 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. const cachedPermissionsHash = storage.getItem(
  19. 'Drupal.contextual.permissionsHash',
  20. );
  21. const permissionsHash = drupalSettings.user.permissionsHash;
  22. if (cachedPermissionsHash !== permissionsHash) {
  23. if (typeof permissionsHash === 'string') {
  24. _.chain(storage)
  25. .keys()
  26. .each(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. * Determines if a contextual link is nested & overlapping, if so: adjusts it.
  36. *
  37. * This only deals with two levels of nesting; deeper levels are not touched.
  38. *
  39. * @param {jQuery} $contextual
  40. * A contextual links placeholder DOM element, containing the actual
  41. * contextual links as rendered by the server.
  42. */
  43. function adjustIfNestedAndOverlapping($contextual) {
  44. const $contextuals = $contextual
  45. // @todo confirm that .closest() is not sufficient
  46. .parents('.contextual-region')
  47. .eq(-1)
  48. .find('.contextual');
  49. // Early-return when there's no nesting.
  50. if ($contextuals.length <= 1) {
  51. return;
  52. }
  53. // If the two contextual links overlap, then we move the second one.
  54. const firstTop = $contextuals.eq(0).offset().top;
  55. const secondTop = $contextuals.eq(1).offset().top;
  56. if (firstTop === secondTop) {
  57. const $nestedContextual = $contextuals.eq(1);
  58. // Retrieve height of nested contextual link.
  59. let height = 0;
  60. const $trigger = $nestedContextual.find('.trigger');
  61. // Elements with the .visually-hidden class have no dimensions, so this
  62. // class must be temporarily removed to the calculate the height.
  63. $trigger.removeClass('visually-hidden');
  64. height = $nestedContextual.height();
  65. $trigger.addClass('visually-hidden');
  66. // Adjust nested contextual link's position.
  67. $nestedContextual.css({ top: $nestedContextual.position().top + height });
  68. }
  69. }
  70. /**
  71. * Initializes a contextual link: updates its DOM, sets up model and views.
  72. *
  73. * @param {jQuery} $contextual
  74. * A contextual links placeholder DOM element, containing the actual
  75. * contextual links as rendered by the server.
  76. * @param {string} html
  77. * The server-side rendered HTML for this contextual link.
  78. */
  79. function initContextual($contextual, html) {
  80. const $region = $contextual.closest('.contextual-region');
  81. const contextual = Drupal.contextual;
  82. $contextual
  83. // Update the placeholder to contain its rendered contextual links.
  84. .html(html)
  85. // Use the placeholder as a wrapper with a specific class to provide
  86. // positioning and behavior attachment context.
  87. .addClass('contextual')
  88. // Ensure a trigger element exists before the actual contextual links.
  89. .prepend(Drupal.theme('contextualTrigger'));
  90. // Set the destination parameter on each of the contextual links.
  91. const destination = `destination=${Drupal.encodePath(
  92. Drupal.url(drupalSettings.path.currentPath),
  93. )}`;
  94. $contextual.find('.contextual-links a').each(function() {
  95. const url = this.getAttribute('href');
  96. const glue = url.indexOf('?') === -1 ? '?' : '&';
  97. this.setAttribute('href', url + glue + destination);
  98. });
  99. // Create a model and the appropriate views.
  100. const model = new contextual.StateModel({
  101. title: $region
  102. .find('h2')
  103. .eq(0)
  104. .text()
  105. .trim(),
  106. });
  107. const viewOptions = $.extend({ el: $contextual, model }, options);
  108. contextual.views.push({
  109. visual: new contextual.VisualView(viewOptions),
  110. aural: new contextual.AuralView(viewOptions),
  111. keyboard: new contextual.KeyboardView(viewOptions),
  112. });
  113. contextual.regionViews.push(
  114. new contextual.RegionView($.extend({ el: $region, model }, options)),
  115. );
  116. // Add the model to the collection. This must happen after the views have
  117. // been associated with it, otherwise collection change event handlers can't
  118. // trigger the model change event handler in its views.
  119. contextual.collection.add(model);
  120. // Let other JavaScript react to the adding of a new contextual link.
  121. $(document).trigger('drupalContextualLinkAdded', {
  122. $el: $contextual,
  123. $region,
  124. model,
  125. });
  126. // Fix visual collisions between contextual link triggers.
  127. adjustIfNestedAndOverlapping($contextual);
  128. }
  129. /**
  130. * Attaches outline behavior for regions associated with contextual links.
  131. *
  132. * Events
  133. * Contextual triggers an event that can be used by other scripts.
  134. * - drupalContextualLinkAdded: Triggered when a contextual link is added.
  135. *
  136. * @type {Drupal~behavior}
  137. *
  138. * @prop {Drupal~behaviorAttach} attach
  139. * Attaches the outline behavior to the right context.
  140. */
  141. Drupal.behaviors.contextual = {
  142. attach(context) {
  143. const $context = $(context);
  144. // Find all contextual links placeholders, if any.
  145. let $placeholders = $context
  146. .find('[data-contextual-id]')
  147. .once('contextual-render');
  148. if ($placeholders.length === 0) {
  149. return;
  150. }
  151. // Collect the IDs for all contextual links placeholders.
  152. const ids = [];
  153. $placeholders.each(function() {
  154. ids.push({
  155. id: $(this).attr('data-contextual-id'),
  156. token: $(this).attr('data-contextual-token'),
  157. });
  158. });
  159. const uncachedIDs = [];
  160. const uncachedTokens = [];
  161. ids.forEach(contextualID => {
  162. const html = storage.getItem(`Drupal.contextual.${contextualID.id}`);
  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(
  171. $context.find(`[data-contextual-id="${contextualID.id}"]`),
  172. html,
  173. );
  174. });
  175. return;
  176. }
  177. uncachedIDs.push(contextualID.id);
  178. uncachedTokens.push(contextualID.token);
  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, 'tokens[]': uncachedTokens },
  187. dataType: 'json',
  188. success(results) {
  189. _.each(results, (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(
  202. `[data-contextual-id="${contextualID}"]`,
  203. );
  204. // Initialize the contextual links.
  205. for (let i = 0; i < $placeholders.length; i++) {
  206. initContextual($placeholders.eq(i), html);
  207. }
  208. }
  209. });
  210. },
  211. });
  212. }
  213. },
  214. };
  215. /**
  216. * Namespace for contextual related functionality.
  217. *
  218. * @namespace
  219. */
  220. Drupal.contextual = {
  221. /**
  222. * The {@link Drupal.contextual.View} instances associated with each list
  223. * element of contextual links.
  224. *
  225. * @type {Array}
  226. */
  227. views: [],
  228. /**
  229. * The {@link Drupal.contextual.RegionView} instances associated with each
  230. * contextual region element.
  231. *
  232. * @type {Array}
  233. */
  234. regionViews: [],
  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([], {
  242. model: Drupal.contextual.StateModel,
  243. });
  244. /**
  245. * A trigger is an interactive element often bound to a click handler.
  246. *
  247. * @return {string}
  248. * A string representing a DOM fragment.
  249. */
  250. Drupal.theme.contextualTrigger = function() {
  251. return '<button class="trigger visually-hidden focusable" type="button"></button>';
  252. };
  253. /**
  254. * Bind Ajax contextual links when added.
  255. *
  256. * @param {jQuery.Event} event
  257. * The `drupalContextualLinkAdded` event.
  258. * @param {object} data
  259. * An object containing the data relevant to the event.
  260. *
  261. * @listens event:drupalContextualLinkAdded
  262. */
  263. $(document).on('drupalContextualLinkAdded', (event, data) => {
  264. Drupal.ajax.bindAjaxLinks(data.$el[0]);
  265. });
  266. })(
  267. jQuery,
  268. Drupal,
  269. drupalSettings,
  270. _,
  271. Backbone,
  272. window.JSON,
  273. window.sessionStorage,
  274. );