function computeMenuTranslation(switcher, optionsElement) { // Calculate the position of a language options element. const switcherRect = switcher.getBoundingClientRect(); // Must be called before optionsElement.clientWidth. optionsElement.style.minWidth = `${Math.max(switcherRect.width, 50)}px`; const isOnTop = switcher.dataset.location === 'top'; const isOnBottom = switcher.dataset.location === 'bottom'; const isOnBottomRight = switcher.dataset.location === 'bottom-right'; const isRTL = document.documentElement.dir === 'rtl' // Stuck on the left side of the switcher. let x = switcherRect.left; if (isOnTop && !isRTL || isOnBottom && isRTL || isOnBottomRight && !isRTL) { // Stuck on the right side of the switcher. x = switcherRect.right - optionsElement.clientWidth; } // Stuck on the top of the switcher. let y = switcherRect.top - window.innerHeight - 10; if (isOnTop) { // Stuck on the bottom of the switcher. y = switcherRect.top - window.innerHeight + optionsElement.clientHeight + switcher.clientHeight + 4; } return { x: x, y: y }; } function toggleMenu(switcher) { const optionsElement = switcher.nextElementSibling; optionsElement.classList.toggle('hx:hidden'); // Calculate the position of a language options element. const translate = computeMenuTranslation(switcher, optionsElement); optionsElement.style.transform = `translate3d(${translate.x}px, ${translate.y}px, 0)`; } function resizeMenu(switcher) { const optionsElement = switcher.nextElementSibling; if (optionsElement.classList.contains('hx:hidden')) return; // Calculate the position of a language options element. const translate = computeMenuTranslation(switcher, optionsElement); optionsElement.style.transform = `translate3d(${translate.x}px, ${translate.y}px, 0)`; } ; // Light / Dark theme toggle (function () { const defaultTheme = 'system' const themes = ["light", "dark"]; const themeToggleButtons = document.querySelectorAll(".hextra-theme-toggle"); const themeToggleOptions = document.querySelectorAll(".hextra-theme-toggle-options p"); function applyTheme(theme) { theme = themes.includes(theme) ? theme : "system"; themeToggleButtons.forEach((btn) => btn.parentElement.dataset.theme = theme ); localStorage.setItem("color-theme", theme); } function switchTheme(theme) { setTheme(theme); applyTheme(theme); } const colorTheme = "color-theme" in localStorage ? localStorage.getItem("color-theme") : defaultTheme; switchTheme(colorTheme); // Add click event handler to the menu items. themeToggleOptions.forEach((option) => { option.addEventListener("click", function (e) { e.preventDefault(); switchTheme(option.dataset.item); }) }) // Add click event handler to the buttons themeToggleButtons.forEach((toggler) => { toggler.addEventListener("click", function (e) { e.preventDefault(); toggleMenu(toggler); }); }); window.addEventListener("resize", () => themeToggleButtons.forEach(resizeMenu)) // Dismiss the menu when clicking outside document.addEventListener('click', (e) => { if (e.target.closest('.hextra-theme-toggle') === null) { themeToggleButtons.forEach((toggler) => { toggler.dataset.state = 'closed'; toggler.nextElementSibling.classList.add('hx:hidden'); }); } }); // Listen for system theme changes window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { if (localStorage.getItem("color-theme") === "system") { setTheme("system"); } }); })(); ; // ; // Hamburger menu for mobile navigation document.addEventListener('DOMContentLoaded', function () { const menu = document.querySelector('.hextra-hamburger-menu'); const sidebarContainer = document.querySelector('.hextra-sidebar-container'); function toggleMenu() { // Toggle the hamburger menu menu.querySelector('svg').classList.toggle('open'); // When the menu is open, we want to show the navigation sidebar sidebarContainer.classList.toggle('hx:max-md:[transform:translate3d(0,-100%,0)]'); sidebarContainer.classList.toggle('hx:max-md:[transform:translate3d(0,0,0)]'); // When the menu is open, we want to prevent the body from scrolling document.body.classList.toggle('hx:overflow-hidden'); document.body.classList.toggle('hx:md:overflow-auto'); } menu.addEventListener('click', (e) => { e.preventDefault(); toggleMenu(); }); // Select all anchor tags in the sidebar container const sidebarLinks = sidebarContainer.querySelectorAll('a'); // Add click event listener to each anchor tag sidebarLinks.forEach(link => { link.addEventListener('click', (e) => { // Check if the href attribute contains a hash symbol (links to a heading) if (link.getAttribute('href') && link.getAttribute('href').startsWith('#')) { // Only dismiss overlay on mobile view if (window.innerWidth < 768) { toggleMenu(); } } }); }); }); ; // Copy button for code blocks document.addEventListener('DOMContentLoaded', function () { const getCopyIcon = () => { const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.innerHTML = ` `; svg.setAttribute('fill', 'none'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('stroke', 'currentColor'); svg.setAttribute('stroke-width', '2'); return svg; } const getSuccessIcon = () => { const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.innerHTML = ` `; svg.setAttribute('fill', 'none'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('stroke', 'currentColor'); svg.setAttribute('stroke-width', '2'); return svg; } document.querySelectorAll('.hextra-code-copy-btn').forEach(function (button) { // Add copy and success icons button.querySelector('.hextra-copy-icon')?.appendChild(getCopyIcon()); button.querySelector('.hextra-success-icon')?.appendChild(getSuccessIcon()); // Add click event listener for copy button button.addEventListener('click', function (e) { e.preventDefault(); // Get the code target const target = button.parentElement.previousElementSibling; let codeElement; if (target.tagName === 'CODE') { codeElement = target; } else { // Select the last code element in case line numbers are present const codeElements = target.querySelectorAll('code'); codeElement = codeElements[codeElements.length - 1]; } if (codeElement) { let code = codeElement.innerText; // Replace double newlines with single newlines in the innerText // as each line inside has trailing newline '\n' if ("lang" in codeElement.dataset) { code = code.replace(/\n\n/g, '\n'); } navigator.clipboard.writeText(code).then(function () { button.classList.add('copied'); setTimeout(function () { button.classList.remove('copied'); }, 1000); }).catch(function (err) { console.error('Failed to copy text: ', err); }); } else { console.error('Target element not found'); } }); }); }); ; (function () { function updateGroup(container, index) { const tabs = Array.from(container.querySelectorAll('.hextra-tabs-toggle')); tabs.forEach((tab, i) => { tab.dataset.state = i === index ? 'selected' : ''; if (i === index) { tab.setAttribute('aria-selected', 'true'); tab.tabIndex = 0; } else { tab.removeAttribute('aria-selected'); tab.removeAttribute('tabindex'); } }); const panelsContainer = container.parentElement.nextElementSibling; if (!panelsContainer) return; Array.from(panelsContainer.children).forEach((panel, i) => { panel.dataset.state = i === index ? 'selected' : ''; if (i === index) { panel.tabIndex = 0; } else { panel.removeAttribute('tabindex'); } }); } const syncGroups = document.querySelectorAll('[data-tab-group]'); syncGroups.forEach((group) => { const key = encodeURIComponent(group.dataset.tabGroup); const saved = localStorage.getItem('hextra-tab-' + key); if (saved !== null) { updateGroup(group, parseInt(saved, 10)); } }); document.querySelectorAll('.hextra-tabs-toggle').forEach((button) => { button.addEventListener('click', function (e) { const container = e.target.parentElement; const index = Array.from(container.querySelectorAll('.hextra-tabs-toggle')).indexOf( e.target ); if (container.dataset.tabGroup) { // Sync behavior: update all tab groups with the same name const tabGroupValue = container.dataset.tabGroup; const key = encodeURIComponent(tabGroupValue); document .querySelectorAll('[data-tab-group="' + tabGroupValue + '"]') .forEach((grp) => updateGroup(grp, index)); localStorage.setItem('hextra-tab-' + key, index.toString()); } else { // Non-sync behavior: update only this specific tab group updateGroup(container, index); } }); }); })(); ; (function () { const languageSwitchers = document.querySelectorAll('.hextra-language-switcher'); languageSwitchers.forEach((switcher) => { switcher.addEventListener('click', (e) => { e.preventDefault(); switcher.dataset.state = switcher.dataset.state === 'open' ? 'closed' : 'open'; toggleMenu(switcher); }); }); window.addEventListener("resize", () => languageSwitchers.forEach(resizeMenu)) // Dismiss language switcher when clicking outside document.addEventListener('click', (e) => { if (e.target.closest('.hextra-language-switcher') === null) { languageSwitchers.forEach((switcher) => { switcher.dataset.state = 'closed'; const optionsElement = switcher.nextElementSibling; optionsElement.classList.add('hx:hidden'); }); } }); })(); ; (function () { const hiddenClass = "hx:hidden"; const dropdownToggles = document.querySelectorAll(".hextra-nav-menu-toggle"); dropdownToggles.forEach((toggle) => { toggle.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); // Close all other dropdowns first dropdownToggles.forEach((otherToggle) => { if (otherToggle !== toggle) { otherToggle.dataset.state = "closed"; const otherMenuItems = otherToggle.nextElementSibling; otherMenuItems.classList.add(hiddenClass); } }); // Toggle current dropdown const isOpen = toggle.dataset.state === "open"; toggle.dataset.state = isOpen ? "closed" : "open"; const menuItemsElement = toggle.nextElementSibling; if (!isOpen) { // Position dropdown centered with toggle menuItemsElement.style.position = "absolute"; menuItemsElement.style.top = "100%"; menuItemsElement.style.left = "50%"; menuItemsElement.style.transform = "translateX(-50%)"; menuItemsElement.style.zIndex = "1000"; // Show dropdown menuItemsElement.classList.remove(hiddenClass); } else { // Hide dropdown menuItemsElement.classList.add(hiddenClass); } }); }); // Dismiss dropdown when clicking outside document.addEventListener("click", (e) => { if (e.target.closest(".hextra-nav-menu-toggle") === null) { dropdownToggles.forEach((toggle) => { toggle.dataset.state = "closed"; const menuItemsElement = toggle.nextElementSibling; menuItemsElement.classList.add(hiddenClass); }); } }); // Close dropdowns on escape key document.addEventListener("keydown", (e) => { if (e.key === "Escape") { dropdownToggles.forEach((toggle) => { toggle.dataset.state = "closed"; const menuItemsElement = toggle.nextElementSibling; menuItemsElement.classList.add(hiddenClass); }); } }); })(); ; // Script for filetree shortcode collapsing/expanding folders used in the theme // ====================================================================== document.addEventListener("DOMContentLoaded", function () { const folders = document.querySelectorAll(".hextra-filetree-folder"); folders.forEach(function (folder) { folder.addEventListener("click", function () { Array.from(folder.children).forEach(function (el) { el.dataset.state = el.dataset.state === "open" ? "closed" : "open"; }); folder.nextElementSibling.dataset.state = folder.nextElementSibling.dataset.state === "open" ? "closed" : "open"; }); }); }); ; document.addEventListener("DOMContentLoaded", function () { scrollToActiveItem(); enableCollapsibles(); }); function enableCollapsibles() { const buttons = document.querySelectorAll(".hextra-sidebar-collapsible-button"); buttons.forEach(function (button) { button.addEventListener("click", function (e) { e.preventDefault(); const list = button.parentElement.parentElement; if (list) { list.classList.toggle("open") } }); }); } function scrollToActiveItem() { const sidebarScrollbar = document.querySelector("aside.hextra-sidebar-container > .hextra-scrollbar"); const activeItems = document.querySelectorAll(".hextra-sidebar-active-item"); const visibleActiveItem = Array.from(activeItems).find(function (activeItem) { return activeItem.getBoundingClientRect().height > 0; }); if (!visibleActiveItem) { return; } const yOffset = visibleActiveItem.clientHeight; const yDistance = visibleActiveItem.getBoundingClientRect().top - sidebarScrollbar.getBoundingClientRect().top; sidebarScrollbar.scrollTo({ behavior: "instant", top: yDistance - yOffset }); } ; // Back to top button document.addEventListener("DOMContentLoaded", function () { const backToTop = document.querySelector("#backToTop"); if (backToTop) { document.addEventListener("scroll", (e) => { if (window.scrollY > 300) { backToTop.classList.remove("hx:opacity-0"); } else { backToTop.classList.add("hx:opacity-0"); } }); } }); function scrollUp() { window.scroll({ top: 0, left: 0, behavior: "smooth", }); } ; /** * TOC Scroll - Highlights active TOC links based on visible headings * * Uses Intersection Observer to track heading visibility and applies * 'hextra-toc-active' class to corresponding TOC links. Selects the * topmost heading when multiple are visible. * * Requires: .hextra-toc element, matching heading IDs, toc.css styles */ document.addEventListener("DOMContentLoaded", function () { const toc = document.querySelector(".hextra-toc"); if (!toc) return; const tocLinks = toc.querySelectorAll('a[href^="#"]'); if (tocLinks.length === 0) return; const headingIds = Array.from(tocLinks).map((link) => link.getAttribute("href").substring(1)); const headings = headingIds.map((id) => document.getElementById(decodeURIComponent(id))).filter(Boolean); if (headings.length === 0) return; let currentActiveLink = null; let isHashNavigation = false; // Create intersection observer const observer = new IntersectionObserver( (entries) => { // Skip observer updates during hash navigation if (isHashNavigation) return; const visibleHeadings = entries.filter((entry) => entry.isIntersecting).map((entry) => entry.target); if (visibleHeadings.length === 0) return; // Find the heading closest to the top of the viewport const topMostHeading = visibleHeadings.reduce((closest, heading) => { const headingTop = heading.getBoundingClientRect().top; const closestTop = closest.getBoundingClientRect().top; return Math.abs(headingTop) < Math.abs(closestTop) ? heading : closest; }); // Encode the id and make it lowercase to match the TOC link const targetId = encodeURIComponent(topMostHeading.id).toLowerCase(); const targetLink = toc.querySelector(`a[href="#${targetId}"]`); if (targetLink && targetLink !== currentActiveLink) { // Remove active class from previous link if (currentActiveLink) { currentActiveLink.classList.remove("hextra-toc-active"); } // Add active class to current link targetLink.classList.add("hextra-toc-active"); currentActiveLink = targetLink; } }, { rootMargin: "-20px 0px -60% 0px", // Adjust sensitivity threshold: [0, 0.1, 0.5, 1], } ); // Observe all headings headings.forEach((heading) => observer.observe(heading)); // Handle direct navigation to page with hash function handleHashNavigation() { const hash = window.location.hash; // already url encoded if (hash) { const targetLink = toc.querySelector(`a[href="${hash}"]`); if (targetLink) { // Disable observer temporarily during hash navigation isHashNavigation = true; if (currentActiveLink) { currentActiveLink.classList.remove("hextra-toc-active"); } targetLink.classList.add("hextra-toc-active"); currentActiveLink = targetLink; // Re-enable observer after scroll settles setTimeout(() => { isHashNavigation = false; }, 500); return; } } } // Handle hash changes navigation window.addEventListener("hashchange", handleHashNavigation); // Handle initial load setTimeout(handleHashNavigation, 100); }); ; // (function () { const faviconEl = document.getElementById("favicon-svg"); const faviconDarkExists = "false" === "true"; if (faviconEl && faviconDarkExists) { const lightFavicon = '/favicon.svg'; const darkFavicon = '/favicon-dark.svg'; const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)"); function updateFavicon(e) { faviconEl.href = e.matches ? darkFavicon : lightFavicon; } // Set favicon on load updateFavicon(darkModeQuery); // Listen for system preference changes darkModeQuery.addEventListener("change", updateFavicon); } })();