PageSourceSearch

https://www.leger.co.uk/scripts/leger/landing-menu.js?v=YvYj78gNrLo6XwxKEqElkLycDL5mKfwd24S7HHv8WD8

js leger.co.uk collected 2026-09-26 08:24:52 UTC 13,467 bytes, 327 lines download raw bytes

1// Landing-page side popover menu - open/close, submenus, ESC, overlay, link-follow.
2// jQuery is provided globally by scripts-combined.min.js (jQuery 3.6).
3(function ($) {
4    if (!$) { return; }
5
6    $(function () {
7        var $menu = $('[data-landing-menu]');
8        if (!$menu.length || $menu.data('landingMenuBound')) { return; }
9        $menu.data('landingMenuBound', true);
10
11        var $body = $('body');
12        var OPEN_CLASS = '-is-open';
13        var BODY_CLASS = '-mobile-nav-is-open';
14        var HAS_SUBMENU_CLASS = '-has-active-submenu';
15        var ACTIVE_CLASS = '-is-active';
16        var ANIMATING_CLASS = '-is-animating';
17        var VISIBLE_CLASS = '-is-visible';
18        var DESKTOP_QUERY = '(min-width: 992px)';
19        var REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';
20        var INLINE_ANIMATION_MS = 320;
21        var desktopMedia = window.matchMedia ? window.matchMedia(DESKTOP_QUERY) : null;
22        var reducedMotionMedia = window.matchMedia ? window.matchMedia(REDUCED_MOTION_QUERY) : null;
23        var activeSubmenuKey = null;
24
25        var $submenuPanel = $menu.find('[data-landing-menu-submenu-panel]');
26        var $submenuToggles = $menu.find('[data-landing-menu-submenu-toggle]');
27        var $inlineSubmenus = $menu.find('[data-landing-menu-submenu]');
28        var $desktopSubmenuLists = $menu.find('[data-landing-menu-submenu-list]');
29        var $groupToggles = $menu.find('[data-submenu-group-toggle]'); // NEW
30
31        function isOpen() { return $menu.hasClass(OPEN_CLASS); }
32        function isDesktop() { return desktopMedia ? desktopMedia.matches : window.innerWidth >= 992; }
33        function hasReducedMotion() { return reducedMotionMedia ? reducedMotionMedia.matches : false; }
34
35        function setHidden($element, hidden) {
36            if (!$element.length) { return; }
37            $element.prop('hidden', hidden);
38            $element.attr('aria-hidden', hidden ? 'true' : 'false');
39        }
40
41        function setFocusable($element, isFocusable) {
42            $element.find('a, button, input, select, textarea, [tabindex]').each(function () {
43                var $focusable = $(this);
44
45                if (isFocusable) {
46                    var previousTabIndex = $focusable.attr('data-landing-menu-prev-tabindex');
47                    if (previousTabIndex) {
48                        $focusable.attr('tabindex', previousTabIndex);
49                    } else {
50                        $focusable.removeAttr('tabindex');
51                    }
52                    $focusable.removeAttr('data-landing-menu-prev-tabindex');
53                    return;
54                }
55
56                if (!$focusable.is('[data-landing-menu-prev-tabindex]') && $focusable.is('[tabindex]')) {
57                    $focusable.attr('data-landing-menu-prev-tabindex', $focusable.attr('tabindex'));
58                }
59                $focusable.attr('tabindex', '-1');
60            });
61        }
62
63        function getSubmenuKey($element) {
64            return $element.attr('data-submenu-key') || $element.attr('data-landing-menu-submenu-toggle');
65        }
66
67        function updateToggleState(key) {
68            $submenuToggles.each(function () {
69                var $toggle = $(this);
70                var isActive = getSubmenuKey($toggle) === key;
71
72                $toggle.attr('aria-expanded', isActive ? 'true' : 'false');
73                $toggle.toggleClass(ACTIVE_CLASS, isActive);
74                $toggle.closest('[data-landing-menu-submenu-item]').toggleClass(ACTIVE_CLASS, isActive);
75            });
76        }
77
78        // NEW: mirrors updateToggleState(), but for the nested group toggles
79        // (Special Interest / Seasonal Holidays) inside the Holiday Types panel.
80        function resetGroupToggles() {
81            $groupToggles.each(function () {
82                var $toggle = $(this);
83                var $target = $('#' + $toggle.attr('aria-controls'));
84                $toggle.attr('aria-expanded', 'false').removeClass(ACTIVE_CLASS);
85                $target.prop('hidden', true);
86            });
87        }
88
89        function hideAllSubmenus() {
90            setHidden($submenuPanel, true);
91            setHidden($inlineSubmenus, true);
92            setHidden($desktopSubmenuLists, true);
93            $inlineSubmenus
94                .removeClass(VISIBLE_CLASS + ' ' + ANIMATING_CLASS)
95                .off('transitionend.landingMenuSubmenu')
96                .css('height', '');
97            setFocusable($inlineSubmenus, false);
98            $menu.removeClass(HAS_SUBMENU_CLASS);
99            resetGroupToggles(); // NEW
100        }
101
102        function openInlineSubmenu($submenu) {
103            if (!$submenu.length) { return; }
104
105            window.clearTimeout($submenu.data('landingMenuTimer'));
106            $submenu.off('transitionend.landingMenuSubmenu');
107            setHidden($submenu, false);
108            setFocusable($submenu, true);
109
110            if (hasReducedMotion()) {
111                $submenu.addClass(VISIBLE_CLASS).css('height', '');
112                return;
113            }
114
115            $submenu
116                .removeClass(VISIBLE_CLASS)
117                .addClass(ANIMATING_CLASS)
118                .css('height', '0px');
119
120            // Commit the collapsed height before moving to the measured open height.
121            $submenu[0].offsetHeight;
122
123            window.requestAnimationFrame(function () {
124                window.requestAnimationFrame(function () {
125                    if ($submenu.prop('hidden')) { return; }
126
127                    $submenu.addClass(VISIBLE_CLASS).css('height', $submenu[0].scrollHeight + 'px');
128                    $submenu.one('transitionend.landingMenuSubmenu', function (e) {
129                        if (e.originalEvent && e.originalEvent.propertyName !== 'height') { return; }
130                        if ($submenu.hasClass(VISIBLE_CLASS)) {
131                            $submenu.removeClass(ANIMATING_CLASS).css('height', 'auto');
132                        }
133                    });
134
135                    $submenu.data('landingMenuTimer', window.setTimeout(function () {
136                        if ($submenu.hasClass(VISIBLE_CLASS)) {
137                            $submenu.removeClass(ANIMATING_CLASS).css('height', 'auto');
138                        }
139                    }, INLINE_ANIMATION_MS + 80));
140                });
141            });
142        }
143
144        function closeInlineSubmenu($submenu, animate) {
145            if (!$submenu.length || $submenu.prop('hidden')) { return; }
146
147            window.clearTimeout($submenu.data('landingMenuTimer'));
148            $submenu.off('transitionend.landingMenuSubmenu');
149            $submenu.attr('aria-hidden', 'true');
150            setFocusable($submenu, false);
151
152            if (!animate || hasReducedMotion()) {
153                $submenu.removeClass(VISIBLE_CLASS + ' ' + ANIMATING_CLASS).css('height', '');
154                setHidden($submenu, true);
155                return;
156            }
157
158            $submenu
159                .addClass(ANIMATING_CLASS)
160                .css('height', $submenu[0].scrollHeight + 'px');
161
162            // Commit the current expanded height before collapsing to zero.
163            $submenu[0].offsetHeight;
164
165            window.requestAnimationFrame(function () {
166                $submenu.removeClass(VISIBLE_CLASS).css('height', '0px');
167            });
168
169            $submenu.one('transitionend.landingMenuSubmenu', function (e) {
170                if (e.originalEvent && e.originalEvent.propertyName !== 'height') { return; }
171                if (!$submenu.hasClass(VISIBLE_CLASS)) {
172                    $submenu.removeClass(ANIMATING_CLASS).css('height', '');
173                    setHidden($submenu, true);
174                }
175            });
176
177            $submenu.data('landingMenuTimer', window.setTimeout(function () {
178                if (!$submenu.hasClass(VISIBLE_CLASS)) {
179                    $submenu.removeClass(ANIMATING_CLASS).css('height', '');
180                    setHidden($submenu, true);
181                }
182            }, INLINE_ANIMATION_MS + 60));
183        }
184
185        function resetSubmenus() {
186            activeSubmenuKey = null;
187            $submenuToggles.attr('aria-expanded', 'false').removeClass(ACTIVE_CLASS);
188            $menu.find('[data-landing-menu-submenu-item]').removeClass(ACTIVE_CLASS);
189            resetGroupToggles(); // NEW
190
191            if (!isDesktop() && isOpen()) {
192                setHidden($submenuPanel, true);
193                setHidden($desktopSubmenuLists, true);
194                $inlineSubmenus.each(function () {
195                    closeInlineSubmenu($(this), true);
196                });
197                $menu.removeClass(HAS_SUBMENU_CLASS);
198                return;
199            }
200
201            hideAllSubmenus();
202        }
203
204        function openSubmenu(key) {
205            activeSubmenuKey = key;
206            updateToggleState(key);
207            resetGroupToggles(); // NEW
208
209            if (isDesktop()) {
210                $inlineSubmenus.each(function () {
211                    closeInlineSubmenu($(this), false);
212                });
213                $desktopSubmenuLists.each(function () {
214                    var $list = $(this);
215                    setHidden($list, getSubmenuKey($list) !== key);
216                });
217                setHidden($submenuPanel, false);
218                $menu.addClass(HAS_SUBMENU_CLASS);
219                return;
220            }
221
222            setHidden($submenuPanel, true);
223            setHidden($desktopSubmenuLists, true);
224            $inlineSubmenus.each(function () {
225                var $submenu = $(this);
226                if (getSubmenuKey($submenu) === key) {
227                    openInlineSubmenu($submenu);
228                } else {
229                    closeInlineSubmenu($submenu, true);
230                }
231            });
232            $menu.removeClass(HAS_SUBMENU_CLASS);
233        }
234
235        function syncSubmenuLayout() {
236            if (activeSubmenuKey) {
237                openSubmenu(activeSubmenuKey);
238                return;
239            }
240
241            resetSubmenus();
242        }
243
244        function openMenu() {
245            if (isOpen()) { return; }
246            $menu.addClass(OPEN_CLASS);
247            $body.addClass(BODY_CLASS);
248            window.setTimeout(function () {
249                $menu.find('[data-landing-menu-close]').first().trigger('focus');
250            }, 200);
251        }
252
253        function closeMenu() {
254            if (!isOpen()) { return; }
255            $menu.removeClass(OPEN_CLASS);
256            $body.removeClass(BODY_CLASS);
257            resetSubmenus();
258        }
259
260        // Hamburger trigger
261        $(document).on('click.landingMenu', '.l-landing-header__btn-nav', function (e) {
262            e.preventDefault();
263            if (isOpen()) { closeMenu(); } else { openMenu(); }
264        });
265
266        // Close button + overlay (delegated on the nav root so both work)
267        $menu.on('click.landingMenu', '[data-landing-menu-close]', function (e) {
268            e.preventDefault();
269            closeMenu();
270        });
271
272        // Desktop opens a right-hand submenu panel. Mobile/tablet opens one accordion at a time.
273        $menu.on('click.landingMenu', '[data-landing-menu-submenu-toggle]', function (e) {
274            var key = getSubmenuKey($(this));
275
276            e.preventDefault();
277            if (!key) { return; }
278
279            if (!isDesktop() && activeSubmenuKey === key) {
280                resetSubmenus();
281                return;
282            }
283
284            openSubmenu(key);
285        });
286
287        // NEW: Special Interest / Seasonal Holidays group toggles - now wired into
288        // the same ACTIVE_CLASS / aria-expanded pattern as the top-level toggles,
289        // and behaves as an accordion (opening one closes the other).
290        $menu.on('click.landingMenu', '[data-submenu-group-toggle]', function (e) {
291            e.preventDefault();
292
293            var $toggle = $(this);
294            var willExpand = $toggle.attr('aria-expanded') !== 'true';
295            var $target = $('#' + $toggle.attr('aria-controls'));
296
297            $groupToggles.not($toggle).each(function () {
298                var $other = $(this);
299                $other.attr('aria-expanded', 'false').removeClass(ACTIVE_CLASS);
300                $('#' + $other.attr('aria-controls')).prop('hidden', true);
301            });
302
303            $toggle.attr('aria-expanded', String(willExpand)).toggleClass(ACTIVE_CLASS, willExpand);
304            $target.prop('hidden', !willExpand);
305        });
306
307        // ESC closes
308        $(document).on('keydown.landingMenu', function (e) {
309            if (e.key === 'Escape' && isOpen()) { closeMenu(); }
310        });
311
312        if (desktopMedia) {
313            if (desktopMedia.addEventListener) {
314                desktopMedia.addEventListener('change', syncSubmenuLayout);
315            } else if (desktopMedia.addListener) {
316                desktopMedia.addListener(syncSubmenuLayout);
317            }
318        } else {
319            $(window).on('resize.landingMenu', syncSubmenuLayout);
320        }
321
322        // Auto-close when following an internal link
323        $menu.on('click.landingMenu', 'a[href]:not([href^="#"])', function () {
324            closeMenu();
325        });
326    });
327})(window.jQuery);

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.