// Back to top button
document.addEventListener("DOMContentLoaded", function () {
const backToTop = document.querySelector("#backToTop");
if (backToTop) {
backToTop.addEventListener("click", scrollUp);
document.addEventListener("scroll", (e) => {
if (window.scrollY > 300) {
backToTop.classList.remove("hx:opacity-0");
backToTop.removeAttribute("tabindex");
} else {
backToTop.classList.add("hx:opacity-0");
backToTop.setAttribute("tabindex", "-1");
}
});
}
});
function scrollUp() {
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
window.scroll({
top: 0,
left: 0,
behavior: prefersReducedMotion ? 'auto' : 'smooth',
});
}
;
//
;
// 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;
}
// Make scrollable code blocks focusable for keyboard users.
const updateScrollableCodeBlocks = () => {
document.querySelectorAll('.hextra-code-block pre, .highlight pre').forEach(function (pre) {
if (pre.scrollWidth > pre.clientWidth) {
pre.setAttribute('tabindex', '0');
} else {
pre.removeAttribute('tabindex');
}
});
};
updateScrollableCodeBlocks();
let resizeRaf;
window.addEventListener('resize', () => {
if (resizeRaf) {
cancelAnimationFrame(resizeRaf);
}
resizeRaf = requestAnimationFrame(updateScrollableCodeBlocks);
});
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');
var originalLabel = button.getAttribute('aria-label');
var copiedLabel = button.dataset.copiedLabel || 'Copied!';
button.setAttribute('aria-label', copiedLabel);
setTimeout(function () {
button.classList.remove('copied');
button.setAttribute('aria-label', originalLabel);
}, 1000);
}).catch(function (err) {
console.error('Failed to copy text: ', err);
});
} else {
console.error('Target element not found');
}
});
});
});
;
//
(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);
}
})();
;
// 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";
});
var newState = folder.nextElementSibling.dataset.state === "open" ? "closed" : "open";
folder.nextElementSibling.dataset.state = newState;
folder.setAttribute('aria-expanded', newState === 'open' ? 'true' : 'false');
});
});
});
;
(function () {
const languageSwitchers = document.querySelectorAll('.hextra-language-switcher');
const closeSwitcher = (switcher, focusSwitcher = false) => {
switcher.dataset.state = 'closed';
switcher.setAttribute('aria-expanded', 'false');
const optionsElement = switcher.nextElementSibling;
optionsElement.classList.add('hx:hidden');
if (focusSwitcher) {
switcher.focus();
}
};
const openSwitcher = (switcher, focusTarget = "none") => {
switcher.dataset.state = 'open';
switcher.setAttribute('aria-expanded', 'true');
const optionsElement = switcher.nextElementSibling;
if (optionsElement.classList.contains('hx:hidden')) {
toggleMenu(switcher);
} else {
resizeMenu(switcher);
}
if (focusTarget !== "none") {
const items = Array.from(optionsElement.querySelectorAll('[role="menuitem"]'));
if (items.length > 0) {
const target = focusTarget === "last" ? items[items.length - 1] : items[0];
target.focus();
}
}
};
languageSwitchers.forEach((switcher) => {
switcher.addEventListener('click', (e) => {
e.preventDefault();
if (switcher.dataset.state === 'open') {
closeSwitcher(switcher);
} else {
openSwitcher(switcher);
}
});
switcher.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
openSwitcher(switcher, 'first');
} else if (e.key === 'ArrowUp') {
e.preventDefault();
openSwitcher(switcher, 'last');
}
});
});
document.querySelectorAll('.hextra-language-options[role=menu]').forEach((menu) => {
menu.addEventListener('keydown', (e) => {
const items = Array.from(menu.querySelectorAll('[role="menuitem"]'));
if (items.length === 0) return;
const currentIndex = items.indexOf(document.activeElement);
let newIndex;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
newIndex = (currentIndex + 1) % items.length;
items[newIndex].focus();
break;
case 'ArrowUp':
e.preventDefault();
newIndex = (currentIndex - 1 + items.length) % items.length;
items[newIndex].focus();
break;
case 'Home':
e.preventDefault();
items[0].focus();
break;
case 'End':
e.preventDefault();
items[items.length - 1].focus();
break;
case 'Escape': {
e.preventDefault();
const switcher = menu.previousElementSibling;
if (switcher) {
closeSwitcher(switcher, true);
}
break;
}
}
});
});
window.addEventListener("resize", () => languageSwitchers.forEach(resizeMenu));
// Dismiss language switcher when clicking outside.
document.addEventListener('click', (e) => {
if (!e.target.closest('.hextra-language-switcher') && !e.target.closest('.hextra-language-options')) {
languageSwitchers.forEach((switcher) => {
closeSwitcher(switcher);
});
}
});
})();
;
// Hamburger menu for mobile navigation
document.addEventListener('DOMContentLoaded', function () {
const menu = document.querySelector('.hextra-hamburger-menu');
const sidebarContainer = document.querySelector('.hextra-sidebar-container');
const mobileQuery = window.matchMedia('(max-width: 767px)');
function isMenuOpen() {
return menu.querySelector('svg').classList.contains('open');
}
// On mobile, the sidebar is off-screen so hide it from assistive tech
function syncAriaHidden() {
if (mobileQuery.matches) {
sidebarContainer.setAttribute('aria-hidden', isMenuOpen() ? 'false' : 'true');
} else {
sidebarContainer.removeAttribute('aria-hidden');
}
}
// Set initial state
syncAriaHidden();
mobileQuery.addEventListener('change', syncAriaHidden);
function toggleMenu(options = {}) {
const { focusOnOpen = true } = options;
// 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');
// Sync aria-expanded and aria-hidden
const isOpen = isMenuOpen();
menu.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
syncAriaHidden();
// Move focus into sidebar when opening, restore when closing
if (isOpen) {
if (focusOnOpen) {
const firstFocusable = sidebarContainer.querySelector('a, button, input, [tabindex="0"]');
if (firstFocusable) firstFocusable.focus();
}
} else {
menu.focus();
}
}
menu.addEventListener('click', (e) => {
e.preventDefault();
// Pointer-initiated clicks on mobile should not force focus into the search input,
// which opens the software keyboard immediately.
toggleMenu({ focusOnOpen: e.detail === 0 });
});
// Close menu on Escape key (mobile only)
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && mobileQuery.matches && isMenuOpen()) {
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();
}
}
});
});
});
;
(function () {
const hiddenClass = "hx:hidden";
const dropdownToggles = document.querySelectorAll(".hextra-nav-menu-toggle");
const closeDropdown = (toggle, focusToggle = false) => {
toggle.dataset.state = "closed";
toggle.setAttribute("aria-expanded", "false");
const menuItemsElement = toggle.nextElementSibling;
menuItemsElement.classList.add(hiddenClass);
if (focusToggle) {
toggle.focus();
}
};
const openDropdown = (toggle, focusTarget = "none") => {
// Close all other dropdowns first.
dropdownToggles.forEach((otherToggle) => {
if (otherToggle !== toggle) {
closeDropdown(otherToggle);
}
});
toggle.dataset.state = "open";
toggle.setAttribute("aria-expanded", "true");
const menuItemsElement = toggle.nextElementSibling;
// 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";
menuItemsElement.classList.remove(hiddenClass);
if (focusTarget !== "none") {
const items = Array.from(menuItemsElement.querySelectorAll('[role="menuitem"]'));
if (items.length > 0) {
const target = focusTarget === "last" ? items[items.length - 1] : items[0];
target.focus();
}
}
};
dropdownToggles.forEach((toggle) => {
toggle.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
// Toggle current dropdown.
const isOpen = toggle.dataset.state === "open";
if (isOpen) {
closeDropdown(toggle);
} else {
openDropdown(toggle);
}
});
toggle.addEventListener("keydown", (e) => {
if (e.key === "ArrowDown") {
e.preventDefault();
openDropdown(toggle, "first");
} else if (e.key === "ArrowUp") {
e.preventDefault();
openDropdown(toggle, "last");
}
});
});
document.querySelectorAll(".hextra-nav-menu-items[role=menu]").forEach((menu) => {
menu.addEventListener("keydown", (e) => {
const items = Array.from(menu.querySelectorAll('[role="menuitem"]'));
if (items.length === 0) return;
const currentIndex = items.indexOf(document.activeElement);
let newIndex;
switch (e.key) {
case "ArrowDown":
e.preventDefault();
newIndex = (currentIndex + 1) % items.length;
items[newIndex].focus();
break;
case "ArrowUp":
e.preventDefault();
newIndex = (currentIndex - 1 + items.length) % items.length;
items[newIndex].focus();
break;
case "Home":
e.preventDefault();
items[0].focus();
break;
case "End":
e.preventDefault();
items[items.length - 1].focus();
break;
case "Escape": {
e.preventDefault();
const toggle = menu.previousElementSibling;
if (toggle) {
closeDropdown(toggle, true);
}
break;
}
}
});
});
// Dismiss dropdown when clicking outside.
document.addEventListener("click", (e) => {
if (!e.target.closest(".hextra-nav-menu-toggle") && !e.target.closest(".hextra-nav-menu-items")) {
dropdownToggles.forEach((toggle) => {
closeDropdown(toggle);
});
}
});
// Close dropdowns on escape key.
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
dropdownToggles.forEach((toggle) => {
if (toggle.dataset.state === "open") {
closeDropdown(toggle, true);
}
});
}
});
})();
;
document.addEventListener('DOMContentLoaded', () => {
// Pre-fetch markdown content for all copy buttons to avoid Safari NotAllowedError
// Safari requires clipboard writes to happen synchronously within user gesture
const copyButtons = document.querySelectorAll('.hextra-page-context-menu-copy');
const contentCache = new Map();
// Pre-fetch content for each button on page load
copyButtons.forEach(button => {
const url = button.dataset.url;
if (url) {
fetch(url)
.then(response => {
if (response.ok) return response.text();
throw new Error('Failed to fetch');
})
.then(markdown => contentCache.set(url, markdown))
.catch(error => console.error('Failed to pre-fetch markdown:', error));
}
});
// Initialize copy buttons with synchronous clipboard access
copyButtons.forEach(button => {
button.addEventListener('click', () => {
const url = button.dataset.url;
const markdown = contentCache.get(url);
if (markdown) {
// Synchronous clipboard write initiation - works in Safari
navigator.clipboard.writeText(markdown)
.then(() => {
button.classList.add('copied');
setTimeout(() => button.classList.remove('copied'), 1000);
})
.catch(error => console.error('Failed to copy markdown:', error));
} else {
// Fallback: fetch and copy (may fail in Safari if content not pre-fetched)
fetch(url)
.then(response => {
if (!response.ok) throw new Error('Failed to fetch');
return response.text();
})
.then(text => {
contentCache.set(url, text);
return navigator.clipboard.writeText(text);
})
.then(() => {
button.classList.add('copied');
setTimeout(() => button.classList.remove('copied'), 1000);
})
.catch(error => console.error('Failed to copy markdown:', error));
}
});
});
// Initialize dropdown toggles
const dropdownToggles = document.querySelectorAll('.hextra-page-context-menu-toggle');
dropdownToggles.forEach(toggle => {
const container = toggle.closest('.hextra-page-context-menu');
const menu = container.querySelector('.hextra-page-context-menu-dropdown');
const chevron = toggle.querySelector('[data-chevron]');
toggle.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = toggle.dataset.state === 'open';
// Close all other dropdowns first
dropdownToggles.forEach(t => {
if (t !== toggle) {
t.dataset.state = 'closed';
t.setAttribute('aria-expanded', 'false');
const otherContainer = t.closest('.hextra-page-context-menu');
const otherMenu = otherContainer.querySelector('.hextra-page-context-menu-dropdown');
const otherChevron = t.querySelector('[data-chevron]');
otherMenu.classList.add('hx:hidden');
if (otherChevron) {
otherChevron.style.transform = '';
}
}
});
// Toggle current
toggle.dataset.state = isOpen ? 'closed' : 'open';
toggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
menu.classList.toggle('hx:hidden', isOpen);
// Rotate chevron icon
if (chevron) {
chevron.style.transform = isOpen ? '' : 'rotate(180deg)';
}
});
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
// Check if click is outside any dropdown container
const isOutside = !e.target.closest('.hextra-page-context-menu');
if (isOutside) {
dropdownToggles.forEach(toggle => {
toggle.dataset.state = 'closed';
toggle.setAttribute('aria-expanded', 'false');
const container = toggle.closest('.hextra-page-context-menu');
const menu = container.querySelector('.hextra-page-context-menu-dropdown');
const chevron = toggle.querySelector('[data-chevron]');
menu.classList.add('hx:hidden');
if (chevron) {
chevron.style.transform = '';
}
});
}
});
// Close dropdown on Escape key and return focus to toggle
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
dropdownToggles.forEach(toggle => {
if (toggle.dataset.state === 'open') {
const container = toggle.closest('.hextra-page-context-menu');
closeDropdown(container);
toggle.focus();
}
});
}
});
// Helper to close dropdown
const closeDropdown = (container) => {
if (!container) return;
const toggle = container.querySelector('.hextra-page-context-menu-toggle');
const menu = container.querySelector('.hextra-page-context-menu-dropdown');
if (!toggle || !menu) return;
const chevron = toggle.querySelector('[data-chevron]');
toggle.dataset.state = 'closed';
toggle.setAttribute('aria-expanded', 'false');
menu.classList.add('hx:hidden');
if (chevron) {
chevron.style.transform = '';
}
};
// Handle dropdown menu copy action
document.querySelectorAll('.hextra-page-context-menu-dropdown button[data-action="copy"]').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const container = btn.closest('.hextra-page-context-menu');
if (!container) return;
const copyBtn = container.querySelector('.hextra-page-context-menu-copy');
if (!copyBtn) return;
closeDropdown(container);
copyBtn.click();
});
});
// Handle dropdown menu view action
document.querySelectorAll('.hextra-page-context-menu-dropdown button[data-action="view"]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const container = btn.closest('.hextra-page-context-menu');
if (!container) return;
const url = btn.dataset.url;
if (!url) return;
closeDropdown(container);
window.open(url, '_blank', 'noopener,noreferrer');
});
});
});
;
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.closest('li');
if (list) {
list.classList.toggle("open");
button.setAttribute('aria-expanded', list.classList.contains('open') ? 'true' : 'false');
}
});
});
}
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
});
}
;
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)`;
}
;
(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.setAttribute('aria-selected', 'false');
tab.tabIndex = -1;
}
});
const panelsContainer = container.parentElement.nextElementSibling;
if (!panelsContainer) return;
Array.from(panelsContainer.children).forEach((panel, i) => {
panel.dataset.state = i === index ? 'selected' : '';
panel.setAttribute('aria-hidden', i === index ? 'false' : 'true');
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 targetButton = e.currentTarget;
const container = targetButton.parentElement;
const index = Array.from(container.querySelectorAll('.hextra-tabs-toggle')).indexOf(
targetButton
);
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);
}
});
// Keyboard navigation for tabs
button.addEventListener('keydown', function (e) {
const container = button.parentElement;
const tabs = Array.from(container.querySelectorAll('.hextra-tabs-toggle'));
const currentIndex = tabs.indexOf(button);
let newIndex;
switch (e.key) {
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
newIndex = (currentIndex + 1) % tabs.length;
break;
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
newIndex = (currentIndex - 1 + tabs.length) % tabs.length;
break;
case 'Home':
e.preventDefault();
newIndex = 0;
break;
case 'End':
e.preventDefault();
newIndex = tabs.length - 1;
break;
default:
return;
}
if (container.dataset.tabGroup) {
const tabGroupValue = container.dataset.tabGroup;
const key = encodeURIComponent(tabGroupValue);
document
.querySelectorAll('[data-tab-group="' + tabGroupValue + '"]')
.forEach((grp) => updateGroup(grp, newIndex));
localStorage.setItem('hextra-tab-' + key, newIndex.toString());
} else {
updateGroup(container, newIndex);
}
tabs[newIndex].focus();
});
});
})();
;
document.addEventListener("DOMContentLoaded", function () {
// Hugo task lists render bare checkboxes; provide an accessible name.
document.querySelectorAll("main#content li > input[type='checkbox']").forEach(function (checkbox) {
if (checkbox.hasAttribute("aria-label") || checkbox.hasAttribute("aria-labelledby")) {
return;
}
var listItem = checkbox.closest("li");
if (!listItem) return;
var labelText = listItem.textContent.replace(/\s+/g, " ").trim();
if (labelText) {
checkbox.setAttribute("aria-label", labelText);
}
});
});
;
// Light / Dark theme toggle
(function () {
const defaultTheme = 'light'
const themes = ["light", "dark"];
const themeToggleButtons = document.querySelectorAll(".hextra-theme-toggle");
const themeToggleOptions = document.querySelectorAll(".hextra-theme-toggle-options button[role=menuitemradio]");
function applyTheme(theme) {
theme = themes.includes(theme) ? theme : "system";
themeToggleButtons.forEach((btn) => btn.parentElement.dataset.theme = theme );
themeToggleOptions.forEach((option) => {
option.setAttribute('aria-checked', option.dataset.item === theme ? 'true' : 'false');
});
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();
toggler.dataset.state = toggler.dataset.state === 'open' ? 'closed' : 'open';
toggleMenu(toggler);
const isOpen = toggler.dataset.state === 'open';
toggler.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
// Focus first menuitem when opening
if (isOpen) {
const firstItem = toggler.nextElementSibling.querySelector('button[role=menuitemradio]');
if (firstItem) firstItem.focus();
}
});
});
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.setAttribute('aria-expanded', 'false');
toggler.nextElementSibling.classList.add('hx:hidden');
});
}
});
// Keyboard navigation for the theme menu
document.querySelectorAll('.hextra-theme-toggle-options[role=menu]').forEach(function (menu) {
menu.addEventListener('keydown', function (e) {
const items = Array.from(menu.querySelectorAll('button[role=menuitemradio]'));
const currentIndex = items.indexOf(document.activeElement);
let newIndex;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
newIndex = (currentIndex + 1) % items.length;
items[newIndex].focus();
break;
case 'ArrowUp':
e.preventDefault();
newIndex = (currentIndex - 1 + items.length) % items.length;
items[newIndex].focus();
break;
case 'Home':
e.preventDefault();
items[0].focus();
break;
case 'End':
e.preventDefault();
items[items.length - 1].focus();
break;
case 'Escape':
e.preventDefault();
var toggler = menu.previousElementSibling;
toggler.dataset.state = 'closed';
toggler.setAttribute('aria-expanded', 'false');
menu.classList.add('hx:hidden');
toggler.focus();
break;
}
});
});
// Listen for system theme changes
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
if (localStorage.getItem("color-theme") === "system") {
setTheme("system");
}
});
})();
;
/**
* 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");
currentActiveLink.removeAttribute("aria-current");
}
// Add active class to current link
targetLink.classList.add("hextra-toc-active");
targetLink.setAttribute("aria-current", "location");
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");
currentActiveLink.removeAttribute("aria-current");
}
targetLink.classList.add("hextra-toc-active");
targetLink.setAttribute("aria-current", "location");
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);
});