Initial Codeberg Pages publish: Doxygen HTML for v0.9.x main
This commit is contained in:
569
menu.js
Normal file
569
menu.js
Normal file
@@ -0,0 +1,569 @@
|
||||
/*
|
||||
@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 */
|
||||
Reference in New Issue
Block a user