PageSourceSearch

https://www.themailworks.com/static/js/nav.js?v=16a09b2ba6

js themailworks.com collected 2026-10-08 10:02:05 UTC 6,358 bytes, 177 lines download raw bytes

1/* ==========================================================================
2   Navigation behaviour.
3
4   The Solutions and About mega-menus open on hover and fold back up when the
5   pointer leaves. The rules:
6     open   on mouseenter over the trigger, its panel, or the bridge between
7            them, no click needed
8     close  on mouseleave, after a short grace delay so travelling across the
9            trigger / bridge / panel never flickers it shut. Also closes on:
10            clicking the trigger again, Escape, clicking outside the nav,
11            opening a different menu, or following a link
12
13   (An earlier version deliberately stayed open until clicked. That was reversed
14   at Owen's request: a menu that hangs open after the pointer has moved on is
15   the more annoying of the two.)
16
17   Keyboard users are not left out: the triggers are real <button> elements, so
18   Enter and Space toggle, and Escape closes and returns focus to the trigger.
19   The live site builds these out of href="#/" anchors, which do none of that.
20   ========================================================================== */
21
22(function () {
23  'use strict';
24
25  var MOBILE_BREAKPOINT = 900;
26  var header = document.querySelector('[data-header]');
27  var dropdowns = Array.prototype.slice.call(document.querySelectorAll('[data-dropdown]'));
28  var burger = document.querySelector('[data-menu-toggle]');
29  var nav = document.getElementById('primary-nav');
30  var hoverCloseTimer;
31
32  if (!dropdowns.length && !burger) return;
33
34  var isMobile = function () { return window.innerWidth < MOBILE_BREAKPOINT; };
35
36  function parts(dd) {
37    return {
38      trigger: dd.querySelector('.primary-nav__trigger'),
39      panel: dd.querySelector('.mega')
40    };
41  }
42
43  function setOpen(dd, open) {
44    var p = parts(dd);
45    if (!p.trigger || !p.panel) return;
46    p.trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
47    dd.classList.toggle('is-open', open);
48    // `hidden` is toggled alongside the class so the panel is genuinely out of
49    // the accessibility tree when closed, not merely transparent.
50    if (open) {
51      p.panel.hidden = false;
52    } else {
53      // Wait for the transition before hiding, otherwise it snaps shut.
54      window.setTimeout(function () {
55        if (!dd.classList.contains('is-open')) p.panel.hidden = true;
56      }, 200);
57    }
58  }
59
60  function closeAll(except) {
61    dropdowns.forEach(function (dd) {
62      if (dd !== except) setOpen(dd, false);
63    });
64  }
65
66  function isOpen(dd) { return dd.classList.contains('is-open'); }
67
68  dropdowns.forEach(function (dd) {
69    var p = parts(dd);
70    if (!p.trigger || !p.panel) return;
71
72    /* --- hover to open, fold back up on mouse-out. ---------------------- */
73    dd.addEventListener('mouseenter', function () {
74      if (isMobile()) return;
75      window.clearTimeout(hoverCloseTimer);
76      closeAll(dd);
77      setOpen(dd, true);
78    });
79
80    // Close on mouse-out, on a short grace delay so moving across the trigger,
81    // the invisible .has-menu::after bridge and the panel does not flicker it
82    // shut. Re-entering (mouseenter above) cancels the pending close.
83    dd.addEventListener('mouseleave', function () {
84      if (isMobile()) return;
85      window.clearTimeout(hoverCloseTimer);
86      hoverCloseTimer = window.setTimeout(function () {
87        setOpen(dd, false);
88      }, 150);
89    });
90
91    /* --- click the trigger to toggle. This is the only pointer gesture that
92           closes a menu, which is exactly what was asked for. -------------- */
93    p.trigger.addEventListener('click', function (e) {
94      e.preventDefault();
95      e.stopPropagation();
96      var open = isOpen(dd);
97      closeAll(dd);
98      setOpen(dd, !open);
99    });
100
101    /* --- keyboard ------------------------------------------------------- */
102    p.trigger.addEventListener('keydown', function (e) {
103      if (e.key === 'ArrowDown') {
104        e.preventDefault();
105        closeAll(dd);
106        setOpen(dd, true);
107        var first = p.panel.querySelector('a');
108        if (first) first.focus();
109      }
110    });
111
112    // Opening by keyboard focus would be surprising for someone tabbing
113    // straight past, so focus alone does not open. Arrow-down does.
114    p.panel.addEventListener('keydown', function (e) {
115      if (e.key === 'Escape') {
116        setOpen(dd, false);
117        p.trigger.focus();
118      }
119    });
120
121    // Following a link should not leave the menu hanging open behind the
122    // new page in a client-side-navigation future.
123    p.panel.addEventListener('click', function (e) {
124      if (e.target.closest('a')) setOpen(dd, false);
125    });
126  });
127
128  /* --- Escape anywhere, and click-outside ------------------------------- */
129  document.addEventListener('keydown', function (e) {
130    if (e.key === 'Escape') closeAll(null);
131  });
132
133  document.addEventListener('click', function (e) {
134    if (nav && !nav.contains(e.target) && !e.target.closest('[data-menu-toggle]')) {
135      closeAll(null);
136    }
137  });
138
139  /* --- Mobile menu ------------------------------------------------------ */
140  if (burger && nav) {
141    burger.addEventListener('click', function () {
142      var open = burger.getAttribute('aria-expanded') === 'true';
143      burger.setAttribute('aria-expanded', open ? 'false' : 'true');
144      document.body.classList.toggle('nav-open', !open);
145      if (open) closeAll(null);
146    });
147
148    // On mobile the panels are accordions inside the drawer, so tapping a
149    // trigger toggles just that section.
150    dropdowns.forEach(function (dd) {
151      var p = parts(dd);
152      if (!p.panel) return;
153      p.panel.hidden = false; // CSS controls visibility in the drawer
154    });
155  }
156
157  /* --- Sticky header shadow once scrolled ------------------------------- */
158  if (header) {
159    var onScroll = function () {
160      header.classList.toggle('is-stuck', window.scrollY > 8);
161    };
162    window.addEventListener('scroll', onScroll, { passive: true });
163    onScroll();
164  }
165
166  /* --- Reset state when crossing the breakpoint ------------------------- */
167  var wasMobile = isMobile();
168  window.addEventListener('resize', function () {
169    var nowMobile = isMobile();
170    if (nowMobile !== wasMobile) {
171      wasMobile = nowMobile;
172      closeAll(null);
173      document.body.classList.remove('nav-open');
174      if (burger) burger.setAttribute('aria-expanded', 'false');
175    }
176  });
177})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.