PageSourceSearch

https://cdn.adsmartfromsky.co.uk/wp-content/themes/skymedia/assets/js/dz-menu-v3.js?ver=3.7.3.3

js adsmartfromsky.co.uk collected 2026-09-28 10:21:54 UTC 39,194 bytes, 1,020 lines download raw bytes

1
2(function ($) {
3    // IconKit compatibility layer:
4    // - supports newer DZIconKit.safeGlobal() / getStyleClass()
5    var IconKit = (window.DZIconKit && window.DZIconKit.safeGlobal) ? window.DZIconKit.safeGlobal() : { htmlFor: () => '', elFor: () => document.createTextNode(''), getStyleClass: () => 'style-symbol' };
6
7    const themeSettings = window.dzSettings || {};
8    const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
9    const settingIsTrue = (value) => value === true || value === '1' || value === 1;
10    const serverSettings = themeSettings.serverDetection || {};
11    const serverDetection = {
12        method: serverSettings.method || themeSettings.detectionMethod || 'unknown',
13        isMobileDevice: settingIsTrue(serverSettings.isMobileDevice ?? themeSettings.isMobileDevice),
14        isMobile: settingIsTrue(serverSettings.isMobile ?? themeSettings.isMobile),
15        isTablet: settingIsTrue(serverSettings.isTablet ?? themeSettings.isTablet),
16        scope: serverSettings.scope || 'cache-generating-request',
17    };
18
19    let $activeItem = null;
20    let hideTimeout;
21    // Strings
22    const subMenuPlaceholderId = '#submenu-display';
23    const headerContainerClass = '.header-strip';
24    const headerMenuButtonClass = '.menu-button';
25    const navClass = '.primary-menu';
26    const menuClass = '.menu';
27    const menuItemClass = '.menu-item';
28    const menuLinkClass = '.menu-link';
29    const menuLinkHasSubmenuClass = '.menu-link.has-submenu';
30    const menuItemHasChildrenClass = '.menu-item-has-children';
31    const menuSubmenuClass = '.submenu';
32    const menuSubmenuSpacerClass = '.submenu-spacer';
33    const menuSubmenuToggleClass = '.submenu-toggle';
34    // CSS classes
35    const menuOpenCSSClass = 'menu-open';
36    //const menuButtonCSSfaClassClosed = 'fa-bars';
37    //const menuButtonCSSfaClassOpen = 'fa-times';
38    const menuIconClosed = 'burger';
39    const menuIconOpen = 'xmark';
40    // Elements
41    const $headerContainer = $(headerContainerClass);
42    const $menu = $(navClass);
43    const $menuItemHasChildren = $(menuItemHasChildrenClass);
44    const $menuButton = $(headerMenuButtonClass);
45    const $submenuToggle = $(menuSubmenuToggleClass);
46    // Placeholder for cloned submenus
47    const $subMenuPlaceholder = $(subMenuPlaceholderId);
48
49    const dzDebugState = {
50        interactionMode: 'unknown',
51        modeReason: '',
52        wrapResult: 'n/a',   // true/false/null/'n/a'
53        menuToggleVisible: 'n/a',
54        menuMeasurable: 'n/a',
55        lastSwitchAt: '',
56        serverDetection,
57        client: {
58            viewportWidth: window.innerWidth,
59            isDesktopViewport: false,
60            isTouchDevice,
61            hasHover: false,
62            hasFinePointer: false,
63        },
64    };
65    function setIcon($el, iconKey) {
66        if (!iconKey) {
67            $el.empty();
68            return;
69        }
70        $el.html(IconKit.htmlFor(iconKey));
71    }
72
73    function isDesktopViewportNow() {
74        return window.matchMedia('(min-width: 768px)').matches;
75    }
76    function updateClientDebugState() {
77        dzDebugState.client = {
78            viewportWidth: window.innerWidth,
79            isDesktopViewport: isDesktopViewportNow(),
80            isTouchDevice,
81            hasHover: window.matchMedia('(hover: hover)').matches,
82            hasFinePointer: window.matchMedia('(pointer: fine)').matches,
83        };
84
85        return dzDebugState.client;
86    }
87    function normalizePath(path) {
88        let p = (path || '/').toString();
89        try {
90            p = decodeURIComponent(p);
91        } catch (e) {
92            // Keep raw path if decoding fails.
93        }
94        p = p.toLowerCase();
95        p = p.replace(/\/index\.php$/, '/');
96        p = p.replace(/\/+/g, '/');
97        p = '/' + p.replace(/^\/+/, '');
98        if (!p.endsWith('/')) p += '/';
99        return p;
100    }
101    function pathFromHref(href) {
102        try {
103            const url = new URL(href, window.location.origin);
104            // Skip links pointing to a different domain.
105            if (url.hostname !== window.location.hostname) return null;
106            return normalizePath(url.pathname || '/');
107        } catch (e) {
108            return null;
109        }
110    }
111    function syncCurrentMenuState() {
112        const $links = $menu.find(menuLinkClass + '[href]');
113        if (!$links.length) return;
114
115        const currentPath = normalizePath(window.location.pathname || '/');
116        const stateClasses = 'current-menu-item current_page_item current-menu-ancestor current_page_ancestor active';
117
118        // Reset any stale classes from fragment-cached markup.
119        $menu.find(menuItemClass).removeClass(stateClasses);
120        $links.removeAttr('aria-current');
121
122        let $best = null;
123        let bestLen = -1;
124
125        // Pick the most specific exact path match.
126        $links.each(function () {
127            const $link = $(this);
128            const itemPath = pathFromHref($link.attr('href'));
129            if (!itemPath) return; // skip external/invalid links
130
131            if (itemPath === currentPath && itemPath.length > bestLen) {
132                $best = $link;
133                bestLen = itemPath.length;
134            }
135        });
136
137        // If no exact match, try to detect parent-child CPT relationship.
138        if (!$best || !$best.length) {
139            const pathSegments = currentPath.split('/').filter(Boolean);
140
141            // If current page has a parent segment (e.g., /case-studies/mathnasium/ => ["case-studies", "mathnasium"])
142            if (pathSegments.length >= 2) {
143                const parentSlug = pathSegments[0];
144
145                // Look for menu links that contain the parent slug in their href.
146                $links.each(function () {
147                    const $link = $(this);
148                    const itemPath = pathFromHref($link.attr('href'));
149                    if (!itemPath) return; // skip external/invalid links
150
151                    // Match if link contains the parent slug (e.g., /case-studies-selection/ contains "case-studies")
152                    if (itemPath.includes('/' + parentSlug) && itemPath.length > bestLen) {
153                        $best = $link;
154                        bestLen = itemPath.length;
155                    }
156                });
157            }
158        }
159
160        if (!$best || !$best.length) return;
161
162        const $currentItem = $best.closest(menuItemClass);
163        $currentItem.addClass('current-menu-item current_page_item active');
164        $best.attr('aria-current', 'page');
165
166        // Mark parents as ancestors for nested menus.
167        $currentItem.parents(menuItemHasChildrenClass).addClass('current-menu-ancestor current_page_ancestor active');
168    }
169    function isTouchDesktopNow() {
170        return isTouchDevice && isDesktopViewportNow();
171    }
172    function bindKeyboardFocusClose() {
173        // Close when focus leaves the currently active item/submenu
174        $menu.on('focusout', function () {
175            // Wait for the browser to move focus, then check where it landed
176            setTimeout(() => {
177                const el = document.activeElement;
178
179                // If focus is still inside the menu or inside the placeholder, keep it open
180                const inMenu = $menu.length && $menu.get(0).contains(el);
181                const inPlaceholder = $subMenuPlaceholder.length && $subMenuPlaceholder.get(0).contains(el);
182
183                if (!inMenu && !inPlaceholder) {
184                    closeSubmenu();
185                }
186            }, 0);
187        });
188
189        // Optional: Esc closes and returns focus to the current trigger link/button
190        $menu.on('keydown', function (e) {
191            if (e.key !== 'Escape') return;
192            e.preventDefault();
193
194            const $returnTo = $activeItem ? $activeItem.find(menuLinkClass).first() : null;
195            closeSubmenu();
196            if ($returnTo && $returnTo.length) $returnTo.focus();
197        });
198    }
199
200    // Run a callback after layout is more likely to be stable
201    function afterLayout(fn) {
202        requestAnimationFrame(() => requestAnimationFrame(fn));
203    }
204    /**
205     * MENU MODE DECISION TREE (keep in sync with dzDebugState.modeReason)
206     *
207     * Goals:
208     *  - Be reversible on resize/rotation (no “stuck in mobile”).
209     *  - Never let CSS-driven burger visibility create a JS deadlock.
210     *  - Prefer capability + layout signals over UA guessing.
211     *  - Keep CSS responsible for layout; JS responsible for interaction.
212     *
213     * Priority order:
214     *  1) forceMode (debug/override)
215     *  2) Burger-visible AND viewport < 768  -> mobile (burger-visible(viewport))
216     *     - IMPORTANT: burger visibility alone is NOT a signal when viewport >= 768,
217     *       because CSS may show burger in mode-mobile/hybrid. Gate by viewport.
218     *  3) Wrap detection (if enabled AND menu measurable):
219     *       wrap=true  -> mobile (wrap-detected)
220     *       wrap=false -> continue
221     *       wrap=null  -> continue (unknown; do not flip modes)
222        *  4) Live viewport/capability fallback:
223        *       viewport < 768 -> mobile
224        *       touch desktop -> hybrid
225        *       else -> desktop
226     *
227     * Binding rules (handleInteractionModeChange):
228     *  - If burger is visible, always ensure mobile click handlers are bound.
229     *  - Rebind not only on mode change, but also on burger visibility change.
230     */
231    function getInteractionMode() {
232        const client = updateClientDebugState();
233
234        if (themeSettings.forceMode) {
235            dzDebugState.modeReason = 'forceMode';
236            return themeSettings.forceMode;
237        }
238        // Check if the menu button is visible
239        const isMenuToggleVisible = $menuButton.is(':visible');
240        dzDebugState.menuToggleVisible = isMenuToggleVisible;
241
242        // Only allow burger-visible to force mobile
243        // IF we are actually in a small viewport
244        if (isMenuToggleVisible && !isDesktopViewportNow()) {
245            dzDebugState.modeReason = 'burger-visible (viewport)';
246            dzDebugState.wrapResult = 'skipped';
247            return 'mobile';
248        }
249        if (isMenuToggleVisible && isDesktopViewportNow()) {
250            dzDebugState.modeReason = 'burger-visible (ignored)';
251        }
252
253        if (themeSettings.useMenuWrapDetection === true) {
254
255            dzDebugState.menuMeasurable = isMeasurable($menu.get(0));
256            const wrapResult = menuItemsAreWrappingSafe($menu);
257            dzDebugState.wrapResult = (wrapResult === null) ? 'not-measurable' : wrapResult;
258
259
260            if (wrapResult === true) {
261                dzDebugState.modeReason = 'wrap-detected';
262                return 'mobile';
263            }
264        } else {
265            dzDebugState.wrapResult = 'disabled';
266        }
267
268        if (!client.isDesktopViewport) {
269            dzDebugState.modeReason = 'mobile-viewport';
270            return 'mobile';
271        }
272        if (isTouchDesktopNow()) {
273            dzDebugState.modeReason = 'touch-desktop';
274            return 'hybrid';
275        }
276        dzDebugState.modeReason = 'desktop-viewport';
277
278        return 'desktop';
279    }
280
281    let interactionMode = 'unknown';
282    let lastInteractionMode = null;
283    let lastBurgerVisible = null;
284
285    const debouncedModeCheck = debounce(() => {
286        afterLayout(() => {
287            handleInteractionModeChange();
288            updateDebugBadge();
289        });
290    }, 200);
291
292    window.addEventListener('resize', debouncedModeCheck);
293    window.addEventListener('orientationchange', debouncedModeCheck);
294
295    if (document.fonts && document.fonts.ready) {
296        document.fonts.ready.then(() => {
297            afterLayout(() => {
298                handleInteractionModeChange();
299                updateDebugBadge();
300            });
301        }).catch(() => { });
302    }
303
304    function debounce(fn, delay) {
305        let timer = null;
306        return function (...args) {
307            clearTimeout(timer);
308            timer = setTimeout(() => fn.apply(this, args), delay);
309        };
310    }
311    function shouldUseHover() {
312        if (isBurgerVisible()) return false;
313        const prefersPointer = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
314        return (interactionMode === 'desktop') || (interactionMode === 'hybrid' && prefersPointer);
315    }
316    function shouldUseClickToggle() {
317        if (isBurgerVisible()) return true;
318        return interactionMode === 'mobile' || (interactionMode === 'hybrid' && !isDesktopViewportNow());
319    }
320    function isBurgerVisible() {
321        return $menuButton.is(':visible');
322    }
323    function isMeasurable(el) {
324        if (!el) return false;
325        if (!el.isConnected) return false;
326        if (el.offsetParent === null) return false; // display:none or not in layout tree
327
328        const r = el.getBoundingClientRect();
329        if (!r || r.width <= 1 || r.height <= 1) return false;
330
331        return true;
332    }
333    function menuItemsAreWrappingSafe($container) {
334        const containerEl = $container && $container.length ? $container.get(0) : null;
335        if (!isMeasurable(containerEl)) return null;
336
337        const $items = $(menuClass + ' > ' + menuItemClass);
338        if ($items.length < 2) return false;
339
340        const firstEl = $items.get(0);
341        if (!isMeasurable(firstEl)) return null;
342
343        // Use getBoundingClientRect (more direct than jQuery offset)
344        const top = firstEl.getBoundingClientRect().top;
345
346        for (const el of $items.get()) {
347            if (!isMeasurable(el)) return null; // if any item isn't measurable, bail
348            if (el.getBoundingClientRect().top > top + 0.5) return true;
349        }
350
351        return false;
352    }
353
354    /**
355     * Get the height of the header container and set it as a CSS variable
356     * @returns {number} - The height of the header container
357     */
358    function getHeaderOffset() {
359        const $header = $headerContainer;
360        const offset = $header.length ? $header.outerHeight() : 110;
361        document.documentElement.style.setProperty('--header-height', offset + 'px');
362        return offset;
363    }
364    // ---- Desktop Submenu functions ----
365    function openSubmenuold($item) {
366        const $submenu = $item.children(menuSubmenuClass);
367        if (!$submenu.length) return;
368
369        const offsetY = getHeaderOffset();
370        const useClone = shouldUseHover();
371
372        $subMenuPlaceholder.css({
373            top: offsetY + 'px',
374            opacity: 1,
375            transform: 'translateY(0)',
376            pointerEvents: 'auto'
377        }).attr('aria-hidden', 'false')
378            .empty();
379
380        if (useClone) {
381            // Remove previous cloned submenu only
382            $subMenuPlaceholder.children(menuSubmenuClass).remove();
383
384            // Clone and append fresh
385            const $cloned = $submenu.clone(true, true);
386            $cloned.attr('data-clone', 'true');
387            $subMenuPlaceholder.append($cloned);
388        } else {
389            // Remove any clone before restoring real submenu
390            $subMenuPlaceholder.children(menuSubmenuClass + '[data-clone="true"]').remove();
391
392            $subMenuPlaceholder.append($submenu);
393        }
394
395        $item.find(menuLinkClass + ', ' + menuSubmenuToggleClass).attr('aria-expanded', 'true');
396        $activeItem = $item;
397    }
398    function openSubmenu($item) {
399        const $submenu = $item.children(menuSubmenuClass);
400        if (!$submenu.length) return;
401
402        // --- KEYBOARD PATH: inline only, no #submenu-display ---
403        if (isKeyboardNav()) {
404            // Ensure the placeholder is not participating at all
405            $subMenuPlaceholder.css({
406                opacity: 0,
407                transform: 'translateY(-10px)',
408                pointerEvents: 'none'
409            }).attr('aria-hidden', 'true')
410                .empty();
411
412            // Open inline submenu (CSS should handle positioning/visibility)
413            $submenu.addClass('open').attr('aria-hidden', 'false');
414
415            $item.find(menuLinkClass + ', ' + menuSubmenuToggleClass).attr('aria-expanded', 'true');
416            $activeItem = $item;
417            return;
418        }
419
420        // --- MOUSE/HOVER PATH: your existing placeholder behaviour ---
421        const offsetY = getHeaderOffset();
422        const useClone = shouldUseHover();
423
424        $subMenuPlaceholder.css({
425            top: offsetY + 'px',
426            opacity: 1,
427            transform: 'translateY(0)',
428            pointerEvents: 'auto'
429        }).attr('aria-hidden', 'false')
430            .empty();
431
432        if (useClone) {
433            $subMenuPlaceholder.children(menuSubmenuClass).remove();
434
435            const $cloned = $submenu.clone(true, true);
436            $cloned.attr('data-clone', 'true');
437            $subMenuPlaceholder.append($cloned);
438        } else {
439            $subMenuPlaceholder.children(menuSubmenuClass + '[data-clone="true"]').remove();
440            $subMenuPlaceholder.append($submenu);
441        }
442
443        $item.find(menuLinkClass + ', ' + menuSubmenuToggleClass).attr('aria-expanded', 'true');
444        $activeItem = $item;
445    }
446
447    function closeSubmenold() {
448        if (!$activeItem) return;
449
450        const $submenu = $subMenuPlaceholder.children(menuSubmenuClass); // match either
451
452        if ($submenu.length && !shouldUseHover()) {
453            // Move back if it was the main menu
454            $menu.append($submenu);
455        }
456
457        $subMenuPlaceholder.css({
458            opacity: 0,
459            transform: 'translateY(-10px)',
460            pointerEvents: 'none'
461        })
462            .attr('aria-hidden', 'true')
463            .empty();
464
465        $activeItem.find(menuLinkClass + ', ' + menuSubmenuToggleClass).attr('aria-expanded', 'false');
466        $activeItem = null;
467    }
468    function closeSubmenu() {
469        if (!$activeItem) return;
470
471        // Close inline submenu if keyboard opened it
472        if (isKeyboardNav()) {
473            const $inline = $activeItem.children(menuSubmenuClass);
474            $inline.removeClass('open').attr('aria-hidden', 'true');
475
476            $activeItem.find(menuLinkClass + ', ' + menuSubmenuToggleClass).attr('aria-expanded', 'false');
477            $activeItem = null;
478            return;
479        }
480
481        // Existing behaviour for placeholder path
482        const $submenu = $subMenuPlaceholder.children(menuSubmenuClass);
483
484        if ($submenu.length && !shouldUseHover()) {
485            $menu.append($submenu);
486        }
487
488        $subMenuPlaceholder.css({
489            opacity: 0,
490            transform: 'translateY(-10px)',
491            pointerEvents: 'none'
492        })
493            .attr('aria-hidden', 'true')
494            .empty();
495
496        $activeItem.find(menuLinkClass + ', ' + menuSubmenuToggleClass).attr('aria-expanded', 'false');
497        $activeItem = null;
498    }
499
500    // ---- END Desktop Submenu functions ----
501
502    // ---- Mobile submenu functions ----
503    /**
504     * Clone the original menu for mobile
505     * This function creates a clone of the original menu and appends it to a placeholder for mobile view.
506     * @param {*} $originalMenu 
507     */
508    function openMenuForMobile($originalMenu) {
509        const $clone = $originalMenu.clone(true, true);
510        const offsetY = getHeaderOffset();
511        // Avoid ID conflicts
512
513        const originalId = $originalMenu.attr('id') || 'primary';
514        const cloneId = originalId + '-mobile';
515
516        $menuButton.attr('aria-controls', cloneId);
517
518        $clone.attr({
519            'id': cloneId,
520            'data-mobile-clone': 'true',
521            'aria-hidden': 'false'
522        });
523
524        // Append to placeholder
525        $subMenuPlaceholder.empty()
526            .css({
527                top: offsetY + 'px',
528                opacity: 1,
529                transform: 'translateY(0)',
530                pointerEvents: 'auto'
531            })
532            .attr('aria-hidden', 'false')
533            .append($clone);
534
535        $clone.css({
536            display: 'flex',
537            visibility: 'visible'
538        });
539    }
540    /**
541     * Close the mobile menu
542     * This function removes the cloned menu from the placeholder and restores the original menu.
543     * @param {*} $originalMenu 
544     */
545    function closeMenuForMobile() {
546        $menuButton.removeAttr('aria-controls');
547
548        $subMenuPlaceholder.css({
549            top: '0px',
550            opacity: 0,
551            transform: 'translateY(-10px)',
552            pointerEvents: 'none'
553        }).attr('aria-hidden', 'true').empty();
554
555    }
556    function hideDesktopMainMenu($originalMenu) {
557        // Hide the original
558        $originalMenu
559            .attr('data-controller', 'hideDMM')
560            .attr('aria-hidden', 'true')
561            .css({
562                display: '',// was none
563                visibility: '' // was hidden
564            })
565            .addClass('dz-nav-hidden');
566    }
567    function restoreDesktopMainMenu($originalMenu) {
568        $originalMenu
569            .attr('data-controller', 'restoreDMM')
570            .attr('aria-hidden', 'false')
571            .css({
572                display: '', //was flex
573                visibility: '' // was visible
574            })
575            .removeClass('dz-nav-hidden');
576    }
577    /**
578     * Mobile inline submenu toggle
579     * @param {*} $item 
580     * @returns 
581     */
582    function openInlineSubmenu($item) {
583        const $submenu = $item.children(menuSubmenuClass);
584        const $spacer = $item.children(menuSubmenuSpacerClass);
585        if (!$submenu.length || !$spacer.length) return;
586
587        closeAllInlineSubmenus();
588
589        $submenu
590            .attr('aria-hidden', 'false')
591            .attr('aria-expanded', 'true')
592            .stop(true, true)
593            .slideDown({
594                duration: 200,
595                step: function (_, fx) {
596                    // Sync spacer height during animation
597                    const currentHeight = $submenu[0]?.getBoundingClientRect?.().height || 0;
598                    $spacer.height(currentHeight);
599                },
600                complete: function () {
601                    const finalHeight = $submenu[0]?.getBoundingClientRect?.().height || 0;
602                    $spacer.height(finalHeight);
603                }
604            });
605
606        $item
607            .attr('aria-expanded', 'true')
608            .find(menuLinkHasSubmenuClass + ', ' + menuSubmenuToggleClass)
609            .attr('aria-expanded', 'true');
610        $activeItem = $item;
611    }
612    function closeInlineSubmenu($item) {
613        const $submenu = $item.children(menuSubmenuClass);
614        const $spacer = $item.children(menuSubmenuSpacerClass);
615        if (!$submenu.length || !$spacer.length) return;
616
617        $submenu
618            .attr('aria-hidden', 'true')
619            .attr('aria-expanded', 'false')
620            .stop(true, true)
621            .slideUp({
622                duration: 200,
623                step: function (_, fx) {
624                    const currentHeight = $submenu[0]?.getBoundingClientRect?.().height || 0;
625                    $spacer.height(currentHeight);
626                },
627                complete: function () {
628                    $spacer.height(0);
629                }
630            });
631
632        $item
633            .attr('aria-expanded', 'false')
634            .find(menuLinkHasSubmenuClass + ', ' + menuSubmenuToggleClass)
635            .attr('aria-expanded', 'false');
636
637        if ($activeItem && $activeItem.is($item)) {
638            $activeItem = null;
639        }
640    }
641    function closeAllInlineSubmenus() {
642        $(menuClass + ' ' + menuSubmenuClass).each(function () {
643            $(this)
644                .attr('aria-hidden', 'true')
645                .attr('aria-expanded', 'false')
646                .stop(true, true)
647                .slideUp(200);
648
649        });
650        $(menuLinkHasSubmenuClass).attr('aria-expanded', 'false');
651        $submenuToggle.attr('aria-expanded', 'false');
652        $activeItem = null;
653    }
654    // ---- END Mobile submenu functions ----
655
656    // ---- Platform Event Binding ----
657    /**
658     * Bind events based on interaction mode
659     * This function binds the appropriate events for desktop/hybrid interaction modes that support hover.
660     */
661    function bindDesktopEvents() {
662        restoreDesktopMainMenu($menu);
663        $menuItemHasChildren.on('mouseenter', function () {
664            if ($('body').hasClass('kb-nav')) exitKeyboardMode();
665            if (!shouldUseHover()) return;
666            clearTimeout(hideTimeout);
667            openSubmenu($(this));
668        });
669
670        $menuItemHasChildren.on('focusin', function () {
671            if (!shouldUseHover()) return;
672            if (!lastInputWasKeyboard) return; // key line: ignore click/right-click focus
673            clearTimeout(hideTimeout);
674            openSubmenu($(this));
675        });
676
677        $menuItemHasChildren.on('mouseleave', function () {
678            if (!shouldUseHover()) return;
679            hideTimeout = setTimeout(closeSubmenu, 300);
680        });
681
682        $subMenuPlaceholder.on('mouseenter', function () {
683            if (!shouldUseHover()) return;
684            clearTimeout(hideTimeout);
685        }).on('mouseleave', function () {
686            if (!shouldUseHover()) return;
687            hideTimeout = setTimeout(closeSubmenu, 300);
688        });
689        $submenuToggle.attr('tabindex', '-1');
690    }
691    function resetMobileToggleMenu() {
692        const $button = $menuButton;
693        $button.attr('aria-expanded', 'false');
694        const $iconWrap = $button.find('.status-icon').first();
695        setIcon($iconWrap, menuIconClosed);
696    }
697    /**
698     * Bind events based on interaction mode
699     * This function binds the appropriate events for mobile interaction modes that support touch.
700     */
701    function bindMobileEvents() {
702        if (themeSettings.debug) console.log('[bindMobileEvents] bound');
703        hideDesktopMainMenu($menu);
704        $menuButton.on('click', function () {
705            const $button = $(this);
706            const $iconWrap = $button.find('.menu-toggle-icon');
707
708            const expanded = $button.attr('aria-expanded') === 'true';
709            $button.attr('aria-expanded', String(!expanded));
710
711            const isOpen = $button.attr('aria-expanded') === 'true';
712            setIcon($iconWrap, isOpen ? menuIconOpen : menuIconClosed);
713
714            let $originalMenu = $menu.first();
715
716            if (!expanded) {
717                $('body').addClass(menuOpenCSSClass);
718
719                openMenuForMobile($originalMenu);
720            } else {
721                $('body').removeClass(menuOpenCSSClass);
722
723                closeMenuForMobile();
724            }
725
726        });
727        let tapTimeout = null;
728        // Add Click handler for mobile submenus, single tap to open, double-tap to follow link
729        // This is a workaround for mobile devices where hover doesn't work well
730        $(menuItemHasChildrenClass + ' > ' + menuLinkClass).on('click', function (e) {
731            const $link = $(this);
732            const $item = $link.closest(menuItemHasChildrenClass);
733            const $submenu = $item.children(menuSubmenuClass);
734
735            if ($submenu.length === 0) return;
735 // skip if no submenu
736
737            if (tapTimeout) {
738                // Double-tap: follow the link
739                clearTimeout(tapTimeout);
740                tapTimeout = null;
741                return true; // let browser handle navigation
742            }
743
744            e.preventDefault(); // prevent immediate nav
745
746            tapTimeout = setTimeout(() => {
747                tapTimeout = null;
748
749                const isOpen = $submenu.attr('aria-hidden') === 'false';
750                if (isOpen) {
751                    closeInlineSubmenu($item);
752                } else {
753                    openInlineSubmenu($item);
754                }
755            }, 300); // 300ms = typical max interval for double tap
756        });
757        // Open/Close submenu on outside click
758        $submenuToggle.on('click', function (e) {
759            e.preventDefault();
760            const $item = $(this).closest(menuItemHasChildrenClass);
761            const isOpen = $(this).attr('aria-expanded') === 'true';
762
763            if (isOpen) {
764                closeInlineSubmenu($item);
765            } else {
766                openInlineSubmenu($item);
767            }
768        });
769        $submenuToggle.removeAttr('tabindex');
770    }
771
772    let dzLastModeLogAt = 0;
773
774    function dzLogModeChange(payload) {
775        const now = Date.now();
776        if (now - dzLastModeLogAt < 5000) return; // 5s throttle
777        dzLastModeLogAt = now;
778
779        // Sentry (if present)
780        if (window.Sentry && typeof window.Sentry.captureMessage === 'function') {
781            window.Sentry.captureMessage('Menu mode change', {
782                level: 'info',
783                tags: { feature: 'menu', mode: payload.mode, reason: payload.reason },
784                extra: payload,
785            });
786        }
787
788        // New Relic Browser (if present)
789        // Depending on the agent version, one of these may exist:
790        if (window.newrelic) {
791            if (typeof window.newrelic.addPageAction === 'function') {
792                window.newrelic.addPageAction('menu_mode_change', payload);
793            } else if (typeof window.newrelic.noticeError === 'function') {
794                // Use noticeError only for real errors; otherwise prefer addPageAction
795                // window.newrelic.noticeError(new Error('menu_mode_change'), payload);
796            }
797        }
798    }
799    function updateDebugBadge() {
800        if (!themeSettings.debug) return;
801
802        let badge = document.getElementById('dz-mode-debug');
803        if (!badge) {
804            badge = document.createElement('div');
805            badge.id = 'dz-mode-debug';
806            badge.style.cssText = 'position:fixed;top:0;left:0;background:#eee;color:#000;padding:6px 8px;font:12px/1.2 monospace;z-index:99999;border-radius:0 0 4px 0;border:1px solid #ccc;';
807            document.body.appendChild(badge);
808        }
809
810        badge.innerHTML = `
811        <strong>Mode:</strong> ${dzDebugState.interactionMode}<br>
812        <span style="opacity:0.8;"><strong>Reason</strong>: ${dzDebugState.modeReason}</span><br>
813        <span style="opacity:0.8;"><strong>Wrap</strong>: ${dzDebugState.wrapResult} | BurgerVisible: ${dzDebugState.menuToggleVisible} | Measurable: ${dzDebugState.menuMeasurable}</span><br>
814        <span style="opacity:0.8;"><strong>Client</strong>: width=${dzDebugState.client.viewportWidth} | desktop=${dzDebugState.client.isDesktopViewport} | touch=${dzDebugState.client.isTouchDevice} | hover=${dzDebugState.client.hasHover} | finePointer=${dzDebugState.client.hasFinePointer}</span><br>
815        <span style="opacity:0.8;"><strong>Server (${dzDebugState.serverDetection.scope})</strong>: method=${dzDebugState.serverDetection.method} | device=${dzDebugState.serverDetection.isMobileDevice} | mobile=${dzDebugState.serverDetection.isMobile} | tablet=${dzDebugState.serverDetection.isTablet}</span><br>
816        <span style="opacity:0.8;"><strong>shouldUseHover</strong>: ${shouldUseHover()} | shouldUseClickToggle: ${shouldUseClickToggle()}</span><br>
817        <span style="opacity:0.8;"><strong>Last switch</strong>: ${dzDebugState.lastSwitchAt || 'n/a'}</span><br>
818        <span style="opacity:0.8;"><strong>burgerMode</strong>: ${isBurgerVisible()}</span><br>
819        <span style="opacity:0.8;"><strong>lastInputWasKeyboard</strong>: ${lastInputWasKeyboard}</span><br>
820        `;
821
822    }
823    function isKeyboardNav() {
824        return lastInputWasKeyboard; //$('body').hasClass('kb-nav');
825    }
826    function exitKeyboardMode() {
827        lastInputWasKeyboard = false;
828        $('body').removeClass('kb-nav');
829    }
830    /**
831     * Keyboard navigation support
832     */
833    let lastInputWasKeyboard = false;
834    let didExitKbOnMouse = false;
835
836    $(document).on('mousedown pointerdown touchstart', function () {
837        // If we were in keyboard mode and an inline submenu is open, close it cleanly
838        if (lastInputWasKeyboard) {
839            closeSubmenu();
840        }
841
842        lastInputWasKeyboard = false;
843        $('body').removeClass('kb-nav');
844        updateDebugBadge();
845    });
846    $(document).on('mousemove', function () {
847        if (!$('body').hasClass('kb-nav')) return;
848        if (didExitKbOnMouse) return;
849        didExitKbOnMouse = true;
850        exitKeyboardMode();
851        updateDebugBadge();
852    });
853
854    $(document).on('keydown', menuLinkClass + ', ' + menuSubmenuToggleClass, function (e) {
855        const $current = $(this).closest(menuItemClass);
856        const $allItems = $(menuItemClass + ':visible');
857        const $submenuLinks = $current.find(menuSubmenuClass + ' ' + menuLinkClass);
858
859        // treat Tab + arrows as keyboard-nav intent
860        if (e.key === 'Tab' || e.key.startsWith('Arrow')) {
861            didExitKbOnMouse = false;
862            lastInputWasKeyboard = true;
863            updateDebugBadge();
864            $('body').addClass('kb-nav');
865        }
866        switch (e.key) {
867            case 'ArrowDown':
868                e.preventDefault();
869                if ($submenuLinks.length) {
870                    $submenuLinks.first().focus();
871                }
872                break;
873
874            case 'ArrowRight':
875                e.preventDefault();
876                const next = $allItems.eq($allItems.index($current) + 1).find(menuLinkClass);
877                if (next.length) next.focus();
878                break;
879
880            case 'ArrowLeft':
881                e.preventDefault();
882                const prev = $allItems.eq($allItems.index($current) - 1).find(menuLinkClass);
883                if (prev.length) prev.focus();
884                break;
885
886            case 'Escape':
887                e.preventDefault();
888                closeSubmenu();
889                $(menuLinkClass + ':focus, ' + menuSubmenuToggleClass + ':focus').blur();
890                break;
891
892            case 'Enter':
893            case ' ':
894                // Only treat as toggle when we're NOT in hover-driven mode
895                if (!shouldUseHover() && $current.hasClass(menuItemHasChildrenClass)) {
896                    e.preventDefault();
897                    const expanded = $current.find(menuLinkClass).attr('aria-expanded') === 'true';
898                    if (!expanded) {
899                        openSubmenu($current);
900                    } else {
901                        closeSubmenu();
902                    }
903                }
904                break;
905        }
906    });
907    // Trap focus inside submenu if open (basic loop)
908    $(document).on('keydown', subMenuPlaceholderId, function (e) {
909        if (e.key !== 'Tab') return;
910
911        const focusable = $subMenuPlaceholder.find('a, button').filter(':visible');
912        const first = focusable.first()[0];
913        const last = focusable.last()[0];
914
915        if (e.shiftKey && document.activeElement === first) {
916            e.preventDefault();
917            last.focus();
918        } else if (!e.shiftKey && document.activeElement === last) {
919            e.preventDefault();
920            first.focus();
921        }
922    });
923    /**
924     * Handle interaction mode changes
925     * This is called on resize and orientation change
926     */
927    function handleInteractionModeChange() {
928        const newMode = getInteractionMode();
929        interactionMode = newMode;
930        dzDebugState.interactionMode = newMode;
931        // clean up first
932        closeSubmenu();
933        closeAllInlineSubmenus();
934        resetMobileToggleMenu();
935        closeMenuForMobile();
936        $('body').removeClass(menuOpenCSSClass); // Remove overlay lock
937        /*
938        // If switching *away* from mobile, restore original menu if needed
939        if (lastInteractionMode === 'mobile' && newMode !== 'mobile') {
940            restoreDesktopMainMenu($menu);
941            const $originalMenu = $(navClass + '[aria-hidden="true"]');
942            // log here
943            console.log('Restoring original menu:', $originalMenu, $originalMenu.length);
944            if ($originalMenu.length) {
945                //closeMenuForMobile($menu);
946            }
947    
948        } else if (lastInteractionMode !== 'mobile' && newMode === 'mobile') {
949            // If switching to mobile, hide original menu
950            hideDesktopMainMenu($menu);
951        }
952    */
953        const burgerVisible = isBurgerVisible();
954        const burgerChanged = (lastBurgerVisible === null) ? true : (burgerVisible !== lastBurgerVisible);
955
956        if (newMode !== lastInteractionMode || !$menu.data('eventsBound') || burgerChanged) {
957            lastBurgerVisible = burgerVisible;
958
959            if (themeSettings.debug) {
960                console.log('[Mode Switch] lastInteractionMode:', lastInteractionMode);
961            }
962            lastInteractionMode = newMode;
963            dzDebugState.lastSwitchAt = new Date().toLocaleTimeString();
964
965            dzLogModeChange({
966                mode: newMode,
967                reason: dzDebugState.modeReason,
968                burgerVisible,
969                burgerChanged,
970                wrap: dzDebugState.wrapResult,
971                measurable: dzDebugState.menuMeasurable,
972                viewportW: window.innerWidth,
973                viewportH: window.innerHeight
974            });
975
976            $menu.data('eventsBound', true);
977
978            $('body')
979                .removeClass('mode-mobile mode-desktop mode-hybrid')
980                .addClass('mode-' + newMode);
981
982            // Unbind everything first
983            $menuItemHasChildren.off();
984            $subMenuPlaceholder.off();
985            $menuButton.off();
986            $submenuToggle.off();
987            $(menuItemHasChildrenClass + ' > ' + menuLinkClass).off();
988
989            // Rebind based on new mode
990            if (isBurgerVisible()) {
991                bindMobileEvents();
992            } else if (shouldUseHover()) {
993                bindDesktopEvents();
994            }
994 else if (shouldUseClickToggle()) {
995                bindMobileEvents();
996            }
997
998            if (themeSettings.debug) {
999                console.log('[Mode Switch] New interactionMode:', newMode);
1000            }
1001        }
1002    }
1003
1004    $(function () {
1005        bindKeyboardFocusClose();
1006        getHeaderOffset();
1007        syncCurrentMenuState();
1008        lastInteractionMode = null;
1009
1010        afterLayout(() => {
1011            handleInteractionModeChange();
1012            syncCurrentMenuState();
1013            updateDebugBadge();
1014        });
1015    });
1016
1017})(jQuery);
1018
1019
1020

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.