| /* |
| @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+='<ul>'; |
| } |
| for (let i in data.children) { |
| let url; |
| const link = data.children[i].url; |
| if (link.substring(0,1)=='^') { |
| url = link.substring(1); |
| } else { |
| url = relPath+link; |
| } |
| result+='<li><a href="'+url+'">'+ |
| data.children[i].text+'</a>'+ |
| makeTree(data.children[i],relPath)+'</li>'; |
| } |
| if (!topLevel) { |
| result+='</ul>'; |
| } |
| } |
| 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 = '<span class="scroll-up-arrow"></span>';//'<span>â–²</span>'; |
| 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 = '<span class="scroll-down-arrow"></span>'; |
| 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 */ |