570 lines
20 KiB
JavaScript
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 */
|