Files
2026-05-04 14:52:32 +02:00

570 lines
20 KiB
JavaScript

/*
@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 */