PageSourceSearch

https://www.voxpopme.com/website/assets/js/blocks/header-2026.js

js voxpopme.com collected 2026-10-01 10:19:31 UTC 4,631 bytes, 152 lines download raw bytes

1/**
2 * Header 2026 — Improved dropdown UX
3 *
4 * Overrides the hover/scroll behaviour from application.js with:
5 * - Intent-based hover delay (prevents flicker)
6 * - No scroll-close (so the menu stays open while browsing)
7 * - Click-outside-to-close
8 * - ESC key closes dropdowns
9 * - Mobile: only one submenu open at a time
10 * - Backdrop overlay when dropdown is open
11 */
12(function () {
13  'use strict';
14
15  document.addEventListener('DOMContentLoaded', function () {
16
17    var OPEN_DELAY = 80;   // ms before opening on hover
18    var CLOSE_DELAY = 250;  // ms before closing on leave (generous for mouse travel)
19
20    var dropdownItems = document.querySelectorAll('.nav-primary > .nav-item--has-children');
21
22    // ── Track which item should be open ──
23    // application.js removes .show on scroll via $(window).on("scroll", ...)
24    // Instead of racing to unbind it, we track intent and restore after scroll.
25    var activeItem = null;
26
27    function restoreAfterScroll() {
28      if (activeItem && getViewportWidth() >= 1024) {
29        // application.js scroll handler runs synchronously and removes .show
30        // We re-apply on the next frame
31        requestAnimationFrame(function () {
32          if (activeItem) {
33            activeItem.classList.add('show');
34            updateBackdrop();
35          }
36        });
37      }
38    }
39
40    window.addEventListener('scroll', restoreAfterScroll, { passive: true });
41
42    // ── Unbind application.js hover handlers ──
43    // Wait a tick so application.js $(document).ready() has fired first
44    setTimeout(function () {
45      if (window.jQuery) {
46        jQuery('.nav-primary .nav-item--has-children').off('mouseenter mouseleave');
47        jQuery('.nav-primary .nav-item--has-children .arrow').off('click');
48      }
49    }, 0);
50
51    // ── Utility ──
52    function getViewportWidth() {
53      return window.innerWidth || document.documentElement.clientWidth;
54    }
55
56    // ── Backdrop overlay ──
57    var backdrop = document.createElement('div');
58    backdrop.className = 'nav-backdrop';
59    document.body.appendChild(backdrop);
60
61    function updateBackdrop() {
62      var anyOpen = Array.prototype.some.call(dropdownItems, function (item) {
63        return item.classList.contains('show');
64      });
65      backdrop.classList.toggle('is-visible', anyOpen);
66    }
67
68    function closeAll() {
69      activeItem = null;
70      dropdownItems.forEach(function (item) {
71        item.classList.remove('show');
72      });
73      updateBackdrop();
74    }
75
76    function closeOthers(current) {
77      dropdownItems.forEach(function (item) {
78        if (item !== current) {
79          item.classList.remove('show');
80        }
81      });
82    }
83
84    // ── Desktop: intent-based hover ──
85    dropdownItems.forEach(function (item) {
86      var openTimer = null;
87      var closeTimer = null;
88
89      item.addEventListener('mouseenter', function () {
90        if (getViewportWidth() < 1024) return;
91
92        clearTimeout(closeTimer);
93        closeTimer = null;
94
95        openTimer = setTimeout(function () {
96          closeOthers(item);
97          activeItem = item;
98          item.classList.add('show');
99          updateBackdrop();
100        }, OPEN_DELAY);
101      });
102
103      item.addEventListener('mouseleave', function () {
104        if (getViewportWidth() < 1024) return;
105
106        clearTimeout(openTimer);
107        openTimer = null;
108
109        closeTimer = setTimeout(function () {
110          activeItem = null;
111          item.classList.remove('show');
112          updateBackdrop();
113        }, CLOSE_DELAY);
114      });
115    });
116
117    // ── Desktop: click outside to close ──
118    document.addEventListener('click', function (e) {
119      if (getViewportWidth() < 1024) return;
120
121      var isInsideNav = e.target.closest('.nav-item--has-children');
122      if (!isInsideNav) {
123        closeAll();
124      }
125    });
126
127    // ── Keyboard: ESC to close ──
128    document.addEventListener('keydown', function (e) {
129      if (e.key === 'Escape' || e.keyCode === 27) {
130        closeAll();
131      }
132    });
133
134    // ── Mobile: tap parent link or arrow to toggle dropdown ──
135    document.addEventListener('click', function (e) {
136      if (getViewportWidth() >= 1024) return;
137
138      var parentItem = e.target.closest('.nav-item--has-children');
139      if (!parentItem) return;
140
141      var link = parentItem.querySelector(':scope > a');
142      if (!link || !link.contains(e.target)) return;
143
144      e.preventDefault();
145
146      closeOthers(parentItem);
147      var isOpen = parentItem.classList.toggle('show');
148      activeItem = isOpen ? parentItem : null;
149      updateBackdrop();
150    });
151  });
152})();

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.