e007bdd4e7
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1083 lines
35 KiB
JavaScript
1083 lines
35 KiB
JavaScript
// 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 = `
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
`;
|
|
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 = `
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
|
`;
|
|
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 <span> 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);
|
|
});
|