contextual.es6.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  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
  172. .find(`[data-contextual-id="${contextualID.id}"]:empty`)
  173. .eq(0),
  174. html,
  175. );
  176. });
  177. return;
  178. }
  179. uncachedIDs.push(contextualID.id);
  180. uncachedTokens.push(contextualID.token);
  181. });
  182. // Perform an AJAX request to let the server render the contextual links
  183. // for each of the placeholders.
  184. if (uncachedIDs.length > 0) {
  185. $.ajax({
  186. url: Drupal.url('contextual/render'),
  187. type: 'POST',
  188. data: { 'ids[]': uncachedIDs, 'tokens[]': uncachedTokens },
  189. dataType: 'json',
  190. success(results) {
  191. _.each(results, (html, contextualID) => {
  192. // Store the metadata.
  193. storage.setItem(`Drupal.contextual.${contextualID}`, html);
  194. // If the rendered contextual links are empty, then the current
  195. // user does not have permission to access the associated links:
  196. // don't render anything.
  197. if (html.length > 0) {
  198. // Update the placeholders to contain its rendered contextual
  199. // links. Usually there will only be one placeholder, but it's
  200. // possible for multiple identical placeholders exist on the
  201. // page (probably because the same content appears more than
  202. // once).
  203. $placeholders = $context.find(
  204. `[data-contextual-id="${contextualID}"]`,
  205. );
  206. // Initialize the contextual links.
  207. for (let i = 0; i < $placeholders.length; i++) {
  208. initContextual($placeholders.eq(i), html);
  209. }
  210. }
  211. });
  212. },
  213. });
  214. }
  215. },
  216. };
  217. /**
  218. * Namespace for contextual related functionality.
  219. *
  220. * @namespace
  221. */
  222. Drupal.contextual = {
  223. /**
  224. * The {@link Drupal.contextual.View} instances associated with each list
  225. * element of contextual links.
  226. *
  227. * @type {Array}
  228. */
  229. views: [],
  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. * A Backbone.Collection of {@link Drupal.contextual.StateModel} instances.
  240. *
  241. * @type {Backbone.Collection}
  242. */
  243. Drupal.contextual.collection = new Backbone.Collection([], {
  244. 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. * Bind Ajax contextual links when added.
  257. *
  258. * @param {jQuery.Event} event
  259. * The `drupalContextualLinkAdded` event.
  260. * @param {object} data
  261. * An object containing the data relevant to the event.
  262. *
  263. * @listens event:drupalContextualLinkAdded
  264. */
  265. $(document).on('drupalContextualLinkAdded', (event, data) => {
  266. Drupal.ajax.bindAjaxLinks(data.$el[0]);
  267. });
  268. })(
  269. jQuery,
  270. Drupal,
  271. drupalSettings,
  272. _,
  273. Backbone,
  274. window.JSON,
  275. window.sessionStorage,
  276. );