/* @licstart The following is the entire license notice for the JavaScript code in this file. The MIT License (MIT) Copyright (C) 1997-2020 by Dimitri van Heesch Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. @licend The above is the entire license notice for the JavaScript code in this file */ function initMenu(relPath,treeview) { const SHOW_DELAY = 250; // 250ms delay before showing const HIDE_DELAY = 500; // 500ms delay before hiding const SLIDE_DELAY = 250; // 250ms slide up/down delay const WHEEL_STEP = 30; // 30 pixel per mouse wheel tick const ARROW_STEP = 5; // 5 pixel when hovering arrow up/down const ARROW_POLL_INTERVAL = 20; // 20ms per arrow up/down check const MOBILE_WIDTH = 768; // switch point for mobile/desktop mode // Helper function for slideDown animation function slideDown(element, duration, callback) { if (element.dataset.animating) return; element.dataset.animating = 'true'; element.style.removeProperty('display'); let display = window.getComputedStyle(element).display; if (display === 'none') display = 'block'; element.style.display = display; const height = element.offsetHeight; element.style.overflow = 'hidden'; element.style.height = 0; element.offsetHeight; // force reflow element.style.transitionProperty = 'height'; element.style.transitionDuration = duration + 'ms'; element.style.height = height + 'px'; window.setTimeout(() => { element.style.removeProperty('height'); element.style.removeProperty('overflow'); element.style.removeProperty('transition-duration'); element.style.removeProperty('transition-property'); delete element.dataset.animating; if (callback) callback(); }, duration); } // Helper function for slideUp animation function slideUp(element, duration, callback) { if (element.dataset.animating) return; element.dataset.animating = 'true'; element.style.transitionProperty = 'height'; element.style.transitionDuration = duration + 'ms'; element.style.height = element.offsetHeight + 'px'; element.offsetHeight; // force reflow element.style.overflow = 'hidden'; element.style.height = 0; window.setTimeout(() => { element.style.display = 'none'; element.style.removeProperty('height'); element.style.removeProperty('overflow'); element.style.removeProperty('transition-duration'); element.style.removeProperty('transition-property'); delete element.dataset.animating; if (callback) callback(); }, duration); } // Helper to create the menu tree structure function makeTree(data,relPath,topLevel=false) { let result=''; if ('children' in data) { if (!topLevel) { result+=''; } } return result; } const mainNav = document.getElementById('main-nav'); if (mainNav && mainNav.children.length > 0) { const firstChild = mainNav.children[0]; firstChild.insertAdjacentHTML('afterbegin', makeTree(menudata, relPath, true)); } const searchBoxPos2 = document.getElementById('searchBoxPos2'); let searchBoxContents = searchBoxPos2 ? searchBoxPos2.innerHTML : ''; const mainMenuState = document.getElementById('main-menu-state'); let prevWidth = 0; const initResizableIfExists = function() { if (typeof initResizableFunc === 'function') initResizableFunc(treeview); } // Dropdown menu functionality to replace smartmenus let closeAllDropdowns = null; // Will be set by initDropdownMenu const isMobile = () => window.innerWidth < MOBILE_WIDTH; if (mainMenuState) { const mainMenu = document.getElementById('main-menu'); const searchBoxPos1 = document.getElementById('searchBoxPos1'); // animate mobile main menu mainMenuState.addEventListener('change', function() { if (this.checked) { slideDown(mainMenu, SLIDE_DELAY, () => { mainMenu.style.display = 'block'; initResizableIfExists(); }); } else { slideUp(mainMenu, SLIDE_DELAY, () => { mainMenu.style.display = 'none'; }); } }); // set default menu visibility const resetState = function() { const newWidth = window.innerWidth; if (newWidth !== prevWidth) { // Close all open dropdown menus when switching between mobile/desktop modes if (closeAllDropdowns) { closeAllDropdowns(); } if (newWidth < MOBILE_WIDTH) { mainMenuState.checked = false; mainMenu.style.display = 'none'; if (searchBoxPos2) { searchBoxPos2.innerHTML = ''; searchBoxPos2.style.display = 'none'; } if (searchBoxPos1) { searchBoxPos1.innerHTML = searchBoxContents; searchBoxPos1.style.display = ''; } } else { mainMenu.style.display = ''; if (searchBoxPos1) { searchBoxPos1.innerHTML = ''; searchBoxPos1.style.display = 'none'; } if (searchBoxPos2) { searchBoxPos2.innerHTML = searchBoxContents; searchBoxPos2.style.display = ''; } } if (typeof searchBox !== 'undefined') { searchBox.CloseResultsWindow(); } prevWidth = newWidth; } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function() { resetState(); initResizableIfExists(); }); } else { resetState(); initResizableIfExists(); } window.addEventListener('resize', resetState); } else { initResizableIfExists(); } function initDropdownMenu() { const mainMenu = document.getElementById('main-menu'); if (!mainMenu) return; const menuItems = mainMenu.querySelectorAll('li'); // Helper function to position nested submenu with viewport checking function positionNestedSubmenu(submenu, link) { const viewport = { height: window.innerHeight, scrollY: window.scrollY }; // Set initial position - top aligned with parent (next to arrow) submenu.style.top = '0'; if (isMobile()) { submenu.style.marginLeft = 0; } else { submenu.style.marginLeft = link.offsetWidth + 'px'; } // Get submenu dimensions and position const submenuRect = submenu.getBoundingClientRect(); const submenuHeight = submenuRect.height; const submenuTop = submenuRect.top; const submenuBottom = submenuRect.bottom+1; // add space for border // Check if submenu fits in viewport const fitsAbove = submenuTop >= 0; const fitsBelow = submenuBottom <= viewport.height; if (!fitsAbove || !fitsBelow) { // Submenu doesn't fit - try to adjust position // Overflows bottom, try to shift up const overflow = submenuBottom - viewport.height; const newTop = Math.max(0, submenuTop-overflow)-submenuTop; submenu.style.top = newTop + 'px'; // Re-check after adjustment const adjustedRect = submenu.getBoundingClientRect(); if (adjustedRect.height > viewport.height) { // Still doesn't fit - enable scrolling enableSubmenuScrolling(submenu, link); } } } // Helper function to enable scrolling for tall submenus function enableSubmenuScrolling(submenu, link) { // Check if scroll arrows already exist if (submenu.dataset.scrollEnabled) return; submenu.dataset.scrollEnabled = 'true'; const viewport = { height: window.innerHeight, scrollY: window.scrollY }; // Position submenu to fill available viewport space const parentRect = link.getBoundingClientRect(); const availableHeight = viewport.height - 2; // Leave some margin submenu.style.maxHeight = availableHeight + 'px'; submenu.style.overflow = 'hidden'; submenu.style.position = 'absolute'; // Create scroll arrows const scrollUpArrow = document.createElement('div'); scrollUpArrow.className = 'submenu-scroll-arrow submenu-scroll-up'; scrollUpArrow.innerHTML = '';//''; scrollUpArrow.style.cssText = 'position:absolute;top:0;left:0;right:0;height:30px;background:transparent;text-align:center;line-height:30px;color:#fff;cursor:pointer;z-index:1000;display:none;'; const scrollDownArrow = document.createElement('div'); scrollDownArrow.className = 'submenu-scroll-arrow submenu-scroll-down'; scrollDownArrow.innerHTML = ''; scrollDownArrow.style.cssText = 'position:absolute;bottom:0;left:0;right:0;height:30px;background:transparent;text-align:center;line-height:30px;color:#fff;cursor:pointer;z-index:1000;'; // Create wrapper for submenu content const scrollWrapper = document.createElement('div'); scrollWrapper.className = 'submenu-scroll-wrapper'; scrollWrapper.style.cssText = 'height:100vh;overflow:hidden;position:relative;'; // Move submenu children to wrapper while (submenu.firstChild) { scrollWrapper.appendChild(submenu.firstChild); } submenu.appendChild(scrollUpArrow); submenu.appendChild(scrollWrapper); submenu.appendChild(scrollDownArrow); let scrollPosition = 0; let scrollInterval = null; function updateScrollArrows() { const maxScroll = scrollWrapper.scrollHeight - availableHeight; scrollUpArrow.style.display = scrollPosition > 0 ? 'block' : 'none'; scrollDownArrow.style.display = scrollPosition < maxScroll ? 'block' : 'none'; } function startScrolling(direction) { if (scrollInterval) return; scrollInterval = setInterval(() => { const maxScroll = scrollWrapper.scrollHeight - availableHeight; if (direction === 'up') { scrollPosition = Math.max(0, scrollPosition - ARROW_STEP); } else { scrollPosition = Math.min(maxScroll, scrollPosition + ARROW_STEP); } scrollWrapper.scrollTop = scrollPosition; updateScrollArrows(); if ((direction === 'up' && scrollPosition === 0) || (direction === 'down' && scrollPosition === maxScroll)) { stopScrolling(); } }, ARROW_POLL_INTERVAL); } function stopScrolling() { if (scrollInterval) { clearInterval(scrollInterval); scrollInterval = null; } } scrollUpArrow.addEventListener('mouseenter', () => startScrolling('up')); scrollUpArrow.addEventListener('mouseleave', stopScrolling); scrollDownArrow.addEventListener('mouseenter', () => startScrolling('down')); scrollDownArrow.addEventListener('mouseleave', stopScrolling); function wheelEvent(e) { e.preventDefault(); e.stopPropagation(); const maxScroll = scrollWrapper.scrollHeight - availableHeight; const wheelDelta = e.deltaY; const scrollAmount = wheelDelta > 0 ? WHEEL_STEP : -WHEEL_STEP; // Scroll 30px per wheel tick scrollPosition = Math.max(0, Math.min(maxScroll, scrollPosition + scrollAmount)); scrollWrapper.scrollTop = scrollPosition; updateScrollArrows(); } // Add mouse wheel scrolling support scrollWrapper.addEventListener('wheel', (e) => wheelEvent(e)); // Also add wheel event to submenu itself to catch events submenu.addEventListener('wheel', function(e) { // Only handle if scrolling is enabled if (submenu.dataset.scrollEnabled) { wheelEvent(e); } }); // Initial arrow state updateScrollArrows(); } // Helper function to clean up scroll arrows function disableSubmenuScrolling(submenu) { if (!submenu.dataset.scrollEnabled) return; delete submenu.dataset.scrollEnabled; // Find and remove scroll elements const scrollArrows = submenu.querySelectorAll('.submenu-scroll-arrow'); const scrollWrapper = submenu.querySelector('.submenu-scroll-wrapper'); if (scrollWrapper) { // Move children back to submenu while (scrollWrapper.firstChild) { submenu.appendChild(scrollWrapper.firstChild); } scrollWrapper.remove(); } scrollArrows.forEach(arrow => arrow.remove()); // Reset styles submenu.style.maxHeight = ''; submenu.style.overflow = ''; } menuItems.forEach(item => { const submenu = item.querySelector('ul'); if (submenu) { const link = item.querySelector('a'); if (link) { // Add class and ARIA attributes for accessibility link.classList.add('has-submenu'); link.setAttribute('aria-haspopup', 'true'); link.setAttribute('aria-expanded', 'false'); // Add sub-arrow indicator const span = document.createElement('span'); span.classList.add('sub-arrow'); link.append(span); // Calculate nesting level for z-index // Root menu (main-menu) is level 200 (above the search box at 102), // first submenus are level 201, etc. let nestingLevel = 200; let currentElement = item.parentElement; while (currentElement && currentElement.id !== 'main-menu') { if (currentElement.tagName === 'UL') { nestingLevel++; } currentElement = currentElement.parentElement; } // Apply z-index based on nesting level // This ensures child menus with shadows appear above parent menus submenu.style.zIndex = nestingLevel + 1; // Check if this is a level 2+ submenu (nested within another dropdown) const isNestedSubmenu = item.parentElement && item.parentElement.id !== 'main-menu'; // Timeout management for smooth menu navigation let showTimeout = null; let hideTimeout = null; // Desktop: show on hover item.addEventListener('mouseenter', function() { if (!isMobile()) { // Clear any pending hide timeout if (hideTimeout) { clearTimeout(hideTimeout); hideTimeout = null; } // Set show timeout showTimeout = setTimeout(() => { // Hide all sibling menus at the same level before showing this one const parentElement = item.parentElement; if (parentElement) { const siblings = parentElement.querySelectorAll(':scope > li'); siblings.forEach(sibling => { if (sibling !== item) { const siblingSubmenu = sibling.querySelector('ul'); const siblingLink = sibling.querySelector('a'); if (siblingSubmenu && siblingLink) { siblingSubmenu.style.display = 'none'; siblingLink.setAttribute('aria-expanded', 'false'); disableSubmenuScrolling(siblingSubmenu); } } }); } submenu.style.display = 'block'; // Only apply positioning for nested submenus (level 2+) if (isNestedSubmenu) { positionNestedSubmenu(submenu, link); } link.setAttribute('aria-expanded', 'true'); showTimeout = null; }, SHOW_DELAY); } }); item.addEventListener('mouseleave', function() { if (!isMobile()) { // Clear any pending show timeout if (showTimeout) { clearTimeout(showTimeout); showTimeout = null; } // Set hide timeout hideTimeout = setTimeout(() => { submenu.style.display = 'none'; link.setAttribute('aria-expanded', 'false'); // Clean up scrolling if enabled disableSubmenuScrolling(submenu); hideTimeout = null; }, HIDE_DELAY); } }); if (isMobile() && isNestedSubmenu) { positionNestedSubmenu(submenu, link); } function toggleMenu() { const isExpanded = link.getAttribute('aria-expanded') === 'true'; if (isExpanded) { slideUp(submenu, SLIDE_DELAY, () => { submenu.style.display = 'none'; link.setAttribute('aria-expanded', 'false'); link.classList.remove('highlighted') disableSubmenuScrolling(submenu); }); } else { slideDown(submenu, SLIDE_DELAY, () => { submenu.style.display = 'block'; link.classList.add('highlighted') link.setAttribute('aria-expanded', 'true'); }); } } // Mobile/Touch: toggle on click link.addEventListener('click', function(e) { if (isMobile()) { e.preventDefault(); toggleMenu(); } }); // Keyboard navigation link.addEventListener('keydown', function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleMenu(); } else if (e.key === 'Escape') { submenu.style.display = 'none'; link.setAttribute('aria-expanded', 'false'); disableSubmenuScrolling(submenu); link.focus(); } }); } } }); // Helper function to close all open dropdown menus closeAllDropdowns = function() { menuItems.forEach(item => { const submenu = item.querySelector('ul'); const link = item.querySelector('a.has-submenu'); if (submenu && link) { disableSubmenuScrolling(submenu); submenu.style.display = 'none'; submenu.style.marginLeft = 0; link.setAttribute('aria-expanded', 'false'); link.classList.remove('highlighted'); } }); }; // Close all dropdown menus when clicking a link (navigation to new page or anchor) const allLinks = mainMenu.querySelectorAll('a'); allLinks.forEach(link => { link.addEventListener('click', function() { // Close dropdowns when navigating (unless it's a has-submenu link in mobile mode) if (!link.classList.contains('has-submenu') || !isMobile()) { if (closeAllDropdowns) { closeAllDropdowns(); } } }); }); } // Initialize dropdown menu behavior initDropdownMenu(); // Close all open menus when browser back button is pressed window.addEventListener('popstate', function() { if (closeAllDropdowns) { closeAllDropdowns(); } }); } /* @license-end */