navigation-utils.es6.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. /**
  2. * @file
  3. * Controls the visibility of desktop navigation.
  4. *
  5. * Shows and hides the desktop navigation based on scroll position and controls
  6. * the functionality of the button that shows/hides the navigation.
  7. */
  8. /* eslint-disable no-inner-declarations */
  9. ((Drupal) => {
  10. /**
  11. * Olivero helper functions.
  12. *
  13. * @namespace
  14. */
  15. Drupal.olivero = {};
  16. /**
  17. * Checks if the mobile navigation button is visible.
  18. *
  19. * @return {boolean}
  20. * True if navButtons is hidden, false if not.
  21. */
  22. function isDesktopNav() {
  23. const navButtons = document.querySelector(
  24. '[data-drupal-selector="mobile-buttons"]',
  25. );
  26. return navButtons
  27. ? window.getComputedStyle(navButtons).getPropertyValue('display') ===
  28. 'none'
  29. : false;
  30. }
  31. Drupal.olivero.isDesktopNav = isDesktopNav;
  32. const stickyHeaderToggleButton = document.querySelector(
  33. '[data-drupal-selector="sticky-header-toggle"]',
  34. );
  35. const siteHeaderFixable = document.querySelector(
  36. '[data-drupal-selector="site-header-fixable"]',
  37. );
  38. /**
  39. * Checks if the sticky header is enabled.
  40. *
  41. * @return {boolean}
  42. * True if sticky header is enabled, false if not.
  43. */
  44. function stickyHeaderIsEnabled() {
  45. return stickyHeaderToggleButton.getAttribute('aria-checked') === 'true';
  46. }
  47. /**
  48. * Save the current sticky header expanded state to localStorage, and set
  49. * it to expire after two weeks.
  50. *
  51. * @param {boolean} expandedState
  52. * Current state of the sticky header button.
  53. */
  54. function setStickyHeaderStorage(expandedState) {
  55. const now = new Date();
  56. const item = {
  57. value: expandedState,
  58. expiry: now.getTime() + 20160000, // 2 weeks from now.
  59. };
  60. localStorage.setItem(
  61. 'Drupal.olivero.stickyHeaderState',
  62. JSON.stringify(item),
  63. );
  64. }
  65. /**
  66. * Toggle the state of the sticky header between always pinned and
  67. * only pinned when scrolled to the top of the viewport.
  68. *
  69. * @param {boolean} pinnedState
  70. * State to change the sticky header to.
  71. */
  72. function toggleStickyHeaderState(pinnedState) {
  73. if (isDesktopNav()) {
  74. if (pinnedState === true) {
  75. siteHeaderFixable.classList.add('is-expanded');
  76. } else {
  77. siteHeaderFixable.classList.remove('is-expanded');
  78. }
  79. stickyHeaderToggleButton.setAttribute('aria-checked', pinnedState);
  80. setStickyHeaderStorage(pinnedState);
  81. }
  82. }
  83. /**
  84. * Return the sticky header's stored state from localStorage.
  85. *
  86. * @return {boolean}
  87. * Stored state of the sticky header.
  88. */
  89. function getStickyHeaderStorage() {
  90. const stickyHeaderState = localStorage.getItem(
  91. 'Drupal.olivero.stickyHeaderState',
  92. );
  93. if (!stickyHeaderState) return false;
  94. const item = JSON.parse(stickyHeaderState);
  95. const now = new Date();
  96. // Compare the expiry time of the item with the current time.
  97. if (now.getTime() > item.expiry) {
  98. // If the item is expired, delete the item from storage and return null.
  99. localStorage.removeItem('Drupal.olivero.stickyHeaderState');
  100. return false;
  101. }
  102. return item.value;
  103. }
  104. // Only enable scroll interactivity if the browser supports Intersection
  105. // Observer.
  106. // @see https://github.com/w3c/IntersectionObserver/blob/master/polyfill/intersection-observer.js#L19-L21
  107. if (
  108. 'IntersectionObserver' in window &&
  109. 'IntersectionObserverEntry' in window &&
  110. 'intersectionRatio' in window.IntersectionObserverEntry.prototype
  111. ) {
  112. const fixableElements = document.querySelectorAll(
  113. '[data-drupal-selector="site-header-fixable"], [data-drupal-selector="social-bar-inner"]',
  114. );
  115. function toggleDesktopNavVisibility(entries) {
  116. if (!isDesktopNav()) return;
  117. entries.forEach((entry) => {
  118. // Firefox doesn't seem to support entry.isIntersecting properly,
  119. // so we check the intersectionRatio.
  120. if (entry.intersectionRatio < 1) {
  121. fixableElements.forEach((el) => el.classList.add('is-fixed'));
  122. } else {
  123. fixableElements.forEach((el) => el.classList.remove('is-fixed'));
  124. }
  125. });
  126. }
  127. /**
  128. * Gets the root margin by checking for various toolbar classes.
  129. *
  130. * @return {string}
  131. * Root margin for the Intersection Observer options object.
  132. */
  133. function getRootMargin() {
  134. let rootMarginTop = 72;
  135. const { body } = document;
  136. if (body.classList.contains('toolbar-fixed')) {
  137. rootMarginTop -= 39;
  138. }
  139. if (
  140. body.classList.contains('toolbar-horizontal') &&
  141. body.classList.contains('toolbar-tray-open')
  142. ) {
  143. rootMarginTop -= 40;
  144. }
  145. return `${rootMarginTop}px 0px 0px 0px`;
  146. }
  147. /**
  148. * Monitor the navigation position.
  149. */
  150. function monitorNavPosition() {
  151. const primaryNav = document.querySelector(
  152. '[data-drupal-selector="site-header"]',
  153. );
  154. const options = {
  155. rootMargin: getRootMargin(),
  156. threshold: [0.999, 1],
  157. };
  158. const observer = new IntersectionObserver(
  159. toggleDesktopNavVisibility,
  160. options,
  161. );
  162. if (primaryNav) {
  163. observer.observe(primaryNav);
  164. }
  165. }
  166. if (stickyHeaderToggleButton) {
  167. stickyHeaderToggleButton.addEventListener('click', () => {
  168. toggleStickyHeaderState(!stickyHeaderIsEnabled());
  169. });
  170. }
  171. // If header is pinned open and a header element gains focus, scroll to the
  172. // top of the page to ensure that the header elements can be seen.
  173. const siteHeaderInner = document.querySelector(
  174. '[data-drupal-selector="site-header-inner"]',
  175. );
  176. if (siteHeaderInner) {
  177. siteHeaderInner.addEventListener('focusin', () => {
  178. if (isDesktopNav() && !stickyHeaderIsEnabled()) {
  179. const header = document.querySelector(
  180. '[data-drupal-selector="site-header"]',
  181. );
  182. const headerNav = header.querySelector(
  183. '[data-drupal-selector="header-nav"]',
  184. );
  185. const headerMargin = header.clientHeight - headerNav.clientHeight;
  186. if (window.scrollY > headerMargin) {
  187. window.scrollTo(0, headerMargin);
  188. }
  189. }
  190. });
  191. }
  192. monitorNavPosition();
  193. setStickyHeaderStorage(getStickyHeaderStorage());
  194. toggleStickyHeaderState(getStickyHeaderStorage());
  195. }
  196. })(Drupal);