diff --git a/docusaurus.config.js b/docusaurus.config.js index a5f65578a..466d5a133 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -270,12 +270,11 @@ const config = { "https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js", async: true, }, - { - src: "/docs/js/custom.js", - async: true, - }, ], - clientModules: resolveGlob.sync(["./src/js/**/*.js"]), + clientModules: [ + './src/client/ConfigNavigationClient.js', + './src/client/DetailsClicksClient.js', + ], themeConfig: ( /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ diff --git a/src/client/ConfigNavigationClient.js b/src/client/ConfigNavigationClient.js new file mode 100644 index 000000000..e5e6116d5 --- /dev/null +++ b/src/client/ConfigNavigationClient.js @@ -0,0 +1,230 @@ +/** + * Config Navigation Client Module + * + * This module provides hydration-safe DOM manipulation for the config reference pages. + * It uses Docusaurus lifecycle methods to ensure DOM updates happen AFTER React hydration. + * + * Key features: + * - Sidebar link highlighting based on current hash + * - Details element expansion/collapse for nested config fields + * - Smooth scrolling to target elements + * - State persistence across navigation + */ + +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; + +let isInitialized = false; +let previousHash = null; + +const getParentDetailsElements = function(element) { + const parents = []; + let current = element; + while (current && current !== document.documentElement) { + if (current.tagName === 'DETAILS') { + parents.push(current); + } + current = current.parentElement; + } + return parents; +}; + +const openDetailsElement = function(detailsEl) { + if (detailsEl && detailsEl.tagName === 'DETAILS' && !detailsEl.open) { + detailsEl.open = true; + detailsEl.setAttribute('data-collapsed', 'false'); + // Expand collapsible content by removing inline styles + const collapsibleContent = detailsEl.querySelector(':scope > div[style]'); + if (collapsibleContent) { + collapsibleContent.style.display = 'block'; + collapsibleContent.style.overflow = 'visible'; + collapsibleContent.style.height = 'auto'; + } + } +}; + +const closeDetailsElement = function(detailsEl) { + if (detailsEl && detailsEl.tagName === 'DETAILS' && detailsEl.open) { + detailsEl.open = false; + detailsEl.setAttribute('data-collapsed', 'true'); + } +}; + +const closeOtherDetails = function(keepOpenSet) { + document.querySelectorAll('details.config-field[open]').forEach(function(el) { + if (!keepOpenSet.has(el)) { + closeDetailsElement(el); + } + }); +}; + +const highlightActiveOnPageLink = function(hash) { + // Use the passed hash parameter, not location.hash (which doesn't update with pushState) + if (!hash) { + return; + } + + const activeHash = hash.substring(1); + + const updateTOC = () => { + // Remove active class from all TOC links + const allLinks = document.querySelectorAll('.table-of-contents a'); + for (let i = 0; i < allLinks.length; i++) { + allLinks[i].classList.remove('table-of-contents__link--active'); + } + + // Add active class to matching TOC links + const activeLinks = document.querySelectorAll(".table-of-contents a[href='#" + activeHash + "']"); + for (let i = 0; i < activeLinks.length; i++) { + activeLinks[i].classList.add('table-of-contents__link--active'); + } + }; + + // Update immediately + updateTOC(); + + // Keep updating after scroll ends to override Docusaurus IntersectionObserver + let scrollEndTimer; + const handleScrollEnd = () => { + clearTimeout(scrollEndTimer); + scrollEndTimer = setTimeout(() => { + updateTOC(); + window.removeEventListener('scroll', handleScrollEnd); + }, 50); + }; + + window.addEventListener('scroll', handleScrollEnd); + handleScrollEnd(); +}; + +const highlightDetailsOnActiveHash = function(activeHash, doNotOpen) { + // NOTE: This function ONLY manages details expansion, NOT highlighting + // Highlighting is managed by DetailsClicksClient to avoid race conditions + + // Find the target element and handle its details + const targetElement = activeHash ? document.getElementById(activeHash) : null; + if (targetElement) { + const parentDetails = getParentDetailsElements(targetElement); + const keepOpenSet = new Set(parentDetails); + + // Close other unrelated details + if (!doNotOpen) { + closeOtherDetails(keepOpenSet); + } + + // Open parent details - NO highlighting here! + for (let i = 0; i < parentDetails.length; i++) { + const element = parentDetails[i]; + + // Open all parent details + if (!doNotOpen) { + element.open = true; + element.setAttribute('data-collapsed', 'false'); + const collapsibleContent = element.querySelector(':scope > div[style]'); + if (collapsibleContent) { + collapsibleContent.style.display = 'block'; + collapsibleContent.style.overflow = 'visible'; + collapsibleContent.style.height = 'auto'; + } + } + } + } +}; + +// NOTE: This does NOT scroll - scrolling is handled by DetailsClicksClient +const handleHashNavigation = function(hash) { + if (!hash) return; + + const targetId = hash.substring(1); + highlightDetailsOnActiveHash(targetId); + highlightActiveOnPageLink(hash); +}; + +// Hash link clicks are handled by DetailsClicksClient to avoid race conditions +const initializeEventHandlers = function() { + // Empty - event handlers managed by DetailsClicksClient +}; + +/** + * Docusaurus lifecycle hook - called before DOM update + */ +export function onRouteUpdate({ location, previousLocation }) { + if (previousLocation) { + previousHash = previousLocation.hash; + } +} + +/** + * Docusaurus lifecycle hook - called after route updates and DOM is ready + * Safely manipulates DOM after React hydration + */ +export function onRouteDidUpdate({ location, previousLocation }) { + if (!ExecutionEnvironment.canUseDOM) { + return; + } + + if (!isInitialized) { + isInitialized = true; + + requestAnimationFrame(() => { + requestAnimationFrame(() => { + initializeEventHandlers(); + + if (location.hash) { + const targetId = location.hash.substring(1); + const targetEl = document.getElementById(targetId); + + handleHashNavigation(location.hash); + + if (targetEl) { + requestAnimationFrame(() => { + const y = targetEl.getBoundingClientRect().top + window.scrollY - 100; + window.scrollTo({ top: y, behavior: 'smooth' }); + }); + } + } + }); + }); + + // For popstate, we DO want to scroll since DetailsClicksClient isn't involved + window.addEventListener('popstate', function() { + const hash = location.hash; + if (!hash) return; + + const targetId = hash.substring(1); + const targetEl = document.getElementById(targetId); + + handleHashNavigation(hash); + + if (targetEl) { + requestAnimationFrame(() => { + const y = targetEl.getBoundingClientRect().top + window.scrollY - 100; + window.scrollTo({ top: y, behavior: 'smooth' }); + }); + } + }); + + // NOTE: hashchange only handles highlighting, NOT scrolling (to avoid race condition) + // Scrolling is handled by DetailsClicksClient's universal click handler + window.addEventListener('hashchange', function(e) { + // Extract hash from newURL to handle pushState correctly + const newHash = e.newURL ? e.newURL.split('#')[1] : ''; + const hashToUse = newHash ? '#' + newHash : ''; + + if (!hashToUse) { + return; + } + + const targetId = hashToUse.substring(1); + highlightActiveOnPageLink(hashToUse); + highlightDetailsOnActiveHash(targetId); + }); + + return; + } + + // Hash changes on subsequent navigations are handled by: + // - hashchange listener (for browser back/forward, URL bar edits) + // - DetailsClicksClient click handler (for link clicks) + previousHash = location.hash; + initializeEventHandlers(); +} diff --git a/src/js/details-clicks.js b/src/client/DetailsClicksClient.js similarity index 66% rename from src/js/details-clicks.js rename to src/client/DetailsClicksClient.js index 6234de47f..cf265b45a 100644 --- a/src/js/details-clicks.js +++ b/src/client/DetailsClicksClient.js @@ -71,19 +71,16 @@ copyButton.style.cssText = ` color: var(--ifm-color-content); transition: all 0.2s; opacity: 0; - margin: 0 4px 0 8px; /* Increased left margin to 8px */ + margin: 0 4px 0 8px; vertical-align: middle; `; const summary = el.querySelector('summary'); if (summary) { - // Find the hash link const hashLink = summary.querySelector('a.hash-link'); if (hashLink) { - // Insert the button right after the hash link hashLink.insertAdjacentElement('afterend', copyButton); - // Show button on summary hover summary.addEventListener('mouseenter', () => { copyButton.style.opacity = '1'; }); @@ -93,7 +90,6 @@ copyButton.style.cssText = ` } } - // Rest of the click handler code remains the same copyButton.addEventListener('click', async function(e) { e.preventDefault(); e.stopPropagation(); @@ -186,11 +182,9 @@ const generateMapYaml = function(parsed, indent = 2) { yaml += `${' '.repeat(indent)}${key}:`; if (value.startsWith('map[')) { - // Handle nested map const nestedParsed = parseMapValue(value); if (nestedParsed) { yaml += generateMapYaml(nestedParsed, indent + 2); - // Only add newline if this isn't the last entry if (index < array.length - 1) { yaml += '\n'; } @@ -199,7 +193,6 @@ const generateMapYaml = function(parsed, indent = 2) { } } else { yaml += ` ${value}`; - // Only add newline if this isn't the last entry if (index < array.length - 1) { yaml += '\n'; } @@ -270,7 +263,6 @@ const generateYaml = function (element, processedElements = new Set()) { yaml += nestedYamls.join('\n'); } } else { - // Function to decode HTML entities const decodeHTMLEntities = function (text) { const txt = document.createElement('textarea'); txt.innerHTML = text; @@ -291,10 +283,9 @@ const generateYaml = function (element, processedElements = new Set()) { .querySelector('summary .config-field-type') ?.textContent?.trim() || ''; - // Helper function to format values appropriately + // Quote strings that could be misinterpreted in YAML const formatValue = function (value) { if (typeof value === 'string') { - // Quote the string if it could be misinterpreted in YAML if ( value === 'true' || value === 'false' || @@ -315,13 +306,12 @@ const generateYaml = function (element, processedElements = new Set()) { } }; - // Updated parseMapString function const parseMapString = function (str) { function parse() { let i = 0; function parseValue() { - if (str.substr(i, 4) === 'map[') { + if (str.substring(i, i + 4) === 'map[') { i += 4; // skip 'map[' const obj = {}; while (str[i] !== ']' && i < str.length) { @@ -358,7 +348,7 @@ const generateYaml = function (element, processedElements = new Set()) { return arr; } else { // Parse until space, ':', ']', or ',' - let start = i; + const start = i; while ( i < str.length && ![' ', ':', ']', ',', '\n'].includes(str[i]) @@ -371,7 +361,7 @@ const generateYaml = function (element, processedElements = new Set()) { function parseKey() { // Parse until ':', space, or ']' - let start = i; + const start = i; while ( i < str.length && ![':', ' ', ']'].includes(str[i]) @@ -391,8 +381,8 @@ const generateYaml = function (element, processedElements = new Set()) { const generateYamlFromObject = function (obj, indentLevel = 1) { const indent = ' '.repeat(indentLevel); let lines = []; - for (let k in obj) { - let v = obj[k]; + for (const k in obj) { + const v = obj[k]; if (Array.isArray(v)) { if (v.length === 0) { lines.push(`${indent}${k}: []`); @@ -439,12 +429,10 @@ const generateYaml = function (element, processedElements = new Set()) { }; if (defaultValue.startsWith('map[')) { - // Map handling const obj = parseMapString(defaultValue); const yamlString = generateYamlFromObject(obj, 1); yaml += '\n' + yamlString; } else if (defaultValue.startsWith('[')) { - // Array handling const trimmedValue = defaultValue.trim(); if ( trimmedValue === '[]' || @@ -463,8 +451,8 @@ const generateYaml = function (element, processedElements = new Set()) { let depth = 0; let currentItem = ''; for (let i = 0; i < arrayContent.length; i++) { - let char = arrayContent[i]; - if (arrayContent.substr(i, 4) === 'map[') { + const char = arrayContent[i]; + if (arrayContent.substring(i, i + 4) === 'map[') { depth++; currentItem += 'map['; i += 3; @@ -512,6 +500,47 @@ const generateYaml = function (element, processedElements = new Set()) { return yaml; }; +const openDetailsElement = function(detailsEl) { + if (detailsEl && detailsEl.tagName === 'DETAILS' && !detailsEl.open) { + detailsEl.open = true; + detailsEl.setAttribute('data-collapsed', 'false'); + // Expand collapsible content by removing inline styles + const collapsibleContent = detailsEl.querySelector(':scope > div[style]'); + if (collapsibleContent) { + collapsibleContent.style.display = 'block'; + collapsibleContent.style.overflow = 'visible'; + collapsibleContent.style.height = 'auto'; + } + } +}; + +const closeDetailsElement = function(detailsEl) { + if (detailsEl && detailsEl.tagName === 'DETAILS' && detailsEl.open) { + detailsEl.open = false; + detailsEl.setAttribute('data-collapsed', 'true'); + } +}; + +const getParentDetailsElements = function(element) { + const parents = []; + let current = element; + while (current && current !== document.documentElement) { + if (current.tagName === 'DETAILS') { + parents.push(current); + } + current = current.parentElement; + } + return parents; +}; + +const closeOtherDetails = function(keepOpenSet) { + document.querySelectorAll('details.config-field[open]').forEach(function(el) { + if (!keepOpenSet.has(el)) { + closeDetailsElement(el); + } + }); +}; + const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEventListener) { const state = new URLSearchParams(window.location.search.substring(1)); @@ -519,21 +548,87 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv return setTimeout(preserveExpansionStates, 100); } - // Add copy buttons to config fields + // Wait for React hydration before manipulating DOM on initial page load with hash + if (!skipEventListener && location.hash && !window.__hydrationComplete) { + const rootElement = document.getElementById('__docusaurus'); + const isHydrated = rootElement && (rootElement._reactRootContainer || rootElement._reactRootContainer === null || Object.keys(rootElement).some(key => key.startsWith('__react'))); + + if (!isHydrated) { + return setTimeout(preserveExpansionStates, 100); + } + + window.__hydrationComplete = true; + return setTimeout(preserveExpansionStates, 50); + } + addCopyButtons(); - document.querySelectorAll('details, .tabs-container').forEach(function(el, index) { + const isInitialCallWithHash = !skipEventListener && location.hash; + + // Debounce to prevent "too many calls" errors with History API + let historyUpdateTimer = null; + const debouncedHistoryUpdate = function(url) { + if (historyUpdateTimer) { + clearTimeout(historyUpdateTimer); + } + historyUpdateTimer = setTimeout(() => { + try { + window.history.replaceState(null, '', url); + } catch (e) { + console.warn('History replaceState failed:', e.message); + } + }, 50); + }; + + // Remove all existing highlights before re-initializing + // This prevents stale highlights from previous navigation + document.querySelectorAll('.-contains-target-link').forEach(el => { + el.classList.remove('-contains-target-link'); + }); + + // Sort elements by depth (deepest first) to process children before parents + // This prevents race condition where both parent and child get highlighted + const elements = Array.from(document.querySelectorAll('details, .tabs-container')); + const elementsWithDepth = elements.map(el => { + let depth = 0; + let current = el.parentElement; + while (current) { + if (current.tagName === 'DETAILS') depth++; + current = current.parentElement; + } + return { el, depth }; + }); + elementsWithDepth.sort((a, b) => b.depth - a.depth); // Deepest first + + elementsWithDepth.forEach(function({ el, depth }, index) { const expansionKey = "x" + (el.id || index); const stateChangeElAll = el.querySelectorAll(':scope > summary, :scope > [role="tablist"] > *'); const anchorLinks = el.querySelectorAll(':scope a[href="' + location.hash + '"]'); - if (anchorLinks.length > 0) { - if (el.querySelectorAll(':scope > summary a[href="' + location.hash + '"]').length > 0) { + const targetId = location.hash.substring(1); + const targetEl = targetId ? document.getElementById(targetId) : null; + const containsTarget = targetEl && (el.id === targetId || el.contains(targetEl)); + + // Check if this element's DIRECT summary (not nested) has a link to the target + const hasSummaryLink = el.querySelectorAll(':scope > summary a[href="' + location.hash + '"]').length > 0; + + // Only highlight if no child details element already has the highlight class + // (children are processed first due to depth sorting) + const childDetailsHighlighted = Array.from(el.querySelectorAll('details')).some( + child => child.classList.contains('-contains-target-link') + ); + + if (anchorLinks.length > 0 || containsTarget) { + if (hasSummaryLink && !childDetailsHighlighted) { el.classList.add("-contains-target-link"); } state.set(expansionKey, 1); + + if (containsTarget) { + openDetailsElement(el); + // NOTE: Initial page load scrolling is handled by ConfigNavigationClient + } } else { - el.classList.remove("-contains-target-link"); state.delete(expansionKey); } @@ -543,14 +638,14 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv const state = new URLSearchParams(window.location.search.substring(1)); if ((el.open && el.getAttribute("data-expandable") != "false") || el.classList.contains("tabs-container")) { if (anchorLinks.length == 1) { - if (anchorLinks[0].getAttribute("href") == location.hash) { - el.classList.add("-contains-target-link"); - } + // NOTE: Highlighting is managed by initializeDetailsElement (initial load) + // and universal click handler (user clicks). Do NOT manage highlights here + // to avoid race conditions and double highlighting. } else { state.set(expansionKey, i); } } else { - el.classList.remove("-contains-target-link"); + // NOTE: Do NOT remove highlight class here - only click handler manages highlights state.delete(expansionKey); } @@ -559,7 +654,7 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv query = '?' + query.replace(/^[?]/, ""); } - window.history.replaceState(null, '', window.location.pathname + query + window.location.hash); + debouncedHistoryUpdate(window.location.pathname + query + window.location.hash); } }; @@ -571,14 +666,19 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv stateChangeEl.addEventListener("click", persistState.bind(stateChangeEl, i + 1)); }); - el.querySelectorAll(':scope > summary a[href^="#"]').forEach(anchorLink => { + const anchorLinks = el.querySelectorAll(':scope > summary a[href^="#"]'); + anchorLinks.forEach(anchorLink => { + anchorLink.setAttribute('data-has-handler', 'true'); + anchorLink.addEventListener("click", (e) => { e.stopImmediatePropagation(); e.stopPropagation(); e.preventDefault(); const newHash = anchorLink.getAttribute("href"); + const targetId = newHash.substring(1); + // Remove highlight from elements that don't link to the new hash document.querySelectorAll(".-contains-target-link").forEach(function(el) { if (el.querySelectorAll(':scope > summary a[href="' + newHash + '"]').length == 0) { el.classList.remove("-contains-target-link"); @@ -589,9 +689,29 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv if (query) { query = '?' + query.replace(/^[?]/, ""); } - window.history.replaceState(null, '', window.location.pathname + query + newHash); + debouncedHistoryUpdate(window.location.pathname + query + newHash); - if (!el.hasAttribute("open")) { + const targetEl = targetId ? document.getElementById(targetId) : null; + if (targetEl) { + const parentDetails = getParentDetailsElements(targetEl); + const keepOpenSet = new Set(parentDetails); + + closeOtherDetails(keepOpenSet); + parentDetails.forEach(openDetailsElement); + + // Add orange border ONLY to the innermost/direct details element + if (parentDetails.length > 0) { + const directTargetDetails = parentDetails[0]; // First element is innermost + directTargetDetails.classList.add("-contains-target-link"); + } + + // replaceState doesn't trigger hashchange, so we must scroll here + requestAnimationFrame(() => { + const targetRect = targetEl.getBoundingClientRect(); + const y = window.scrollY + targetRect.top - 100; + window.scrollTo({ top: y, behavior: 'smooth' }); + }); + } else if (!el.hasAttribute("open")) { anchorLink.parentElement.click(); } }); @@ -609,18 +729,12 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv }); if (!skipEventListener) { - window.addEventListener('hashchange', function() { - if (document.querySelectorAll('details.config-field').length > 0) { - setTimeout(addCopyButtons, 200); - } - }); - document.addEventListener('visibilitychange', function() { if (!document.hidden && document.querySelectorAll('details.config-field').length > 0) { setTimeout(addCopyButtons, 100); } }); - + // Use interval check instead of MutationObserver to avoid React conflicts let checkCount = 0; const maxChecks = 10; @@ -638,32 +752,105 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv } } : () => {}; +// ============================================================================ +// Docusaurus Lifecycle Hooks +// ============================================================================ + +// Universal hash link handler (including TOC sidebar) +// Fixes Docusaurus's buggy hash navigation and enables CSS :target highlighting if (ExecutionEnvironment.canUseDOM) { - preserveExpansionStates(); - - // Ensure copy buttons are added when DOM is fully loaded - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', function() { - setTimeout(addCopyButtons, 100); - }); - } else { - setTimeout(addCopyButtons, 100); - } + let scrollTimeout = null; + + document.addEventListener('click', function(e) { + let target = e.target; + let anchor = null; - if (location.hash) { - setTimeout(() => { - location.href = location.href; + while (target && target !== document) { + if (target.tagName === 'A') { + anchor = target; + break; + } + target = target.parentElement; + } - const targetEl = document.querySelector('[id="' + location.hash.substr(1) + '"]'); + if (anchor && anchor.getAttribute('href')?.startsWith('#')) { + const hash = anchor.getAttribute('href'); + const targetId = hash.substring(1); + + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + + const targetEl = targetId ? document.getElementById(targetId) : null; if (targetEl) { - window.scroll({ - behavior: 'smooth', - left: 0, - top: targetEl.getBoundingClientRect().top + window.scrollY - 120 + const parentDetails = getParentDetailsElements(targetEl); + const keepOpenSet = new Set(parentDetails); + closeOtherDetails(keepOpenSet); + parentDetails.forEach(openDetailsElement); + + // Remove all existing highlights first + document.querySelectorAll('.-contains-target-link').forEach(el => { + el.classList.remove('-contains-target-link'); + }); + + // Add highlight to the innermost parent details element + if (parentDetails.length > 0) { + parentDetails[0].classList.add('-contains-target-link'); + } + + // Update URL hash for history/bookmarking + const oldURL = window.location.href; + window.history.pushState(null, '', hash); + const newURL = window.location.href; + + // Trigger hashchange event manually for other listeners (including sidebar highlighting) + window.dispatchEvent(new HashChangeEvent('hashchange', { + oldURL: oldURL, + newURL: newURL + })); + + // Cancel any pending scroll + if (scrollTimeout) { + clearTimeout(scrollTimeout); + } + + // Smooth scroll to target with proper offset + scrollTimeout = requestAnimationFrame(() => { + const rect = targetEl.getBoundingClientRect(); + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + const targetY = scrollTop + rect.top - 100; + + window.scrollTo({ + top: targetY, + behavior: 'smooth' + }); }); } - }, 1000); - } + } + }, true); // Capture phase } -export default ExecutionEnvironment.canUseDOM ? preserveExpansionStates : () => {}; \ No newline at end of file +let isInitialized = false; + +/** + * Docusaurus lifecycle hook - called after route updates and DOM is ready + * Ensures DOM manipulation happens AFTER React hydration + */ +export function onRouteDidUpdate({ location }) { + if (!ExecutionEnvironment.canUseDOM) { + return; + } + + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (!isInitialized) { + isInitialized = true; + preserveExpansionStates(); + } else { + preserveExpansionStates(true); + } + + addCopyButtons(); + }); + }); +} \ No newline at end of file diff --git a/static/js/custom.js b/static/js/custom.js deleted file mode 100644 index cc25f842a..000000000 --- a/static/js/custom.js +++ /dev/null @@ -1,185 +0,0 @@ -var firstCall = true; - -const highlightDetailsOnActiveHash = function(activeHash, doNotOpen) { - const activeAnchors = document.querySelectorAll(".anchor[id='" + activeHash + "'"); - const detailsElements = document.querySelectorAll("details"); - const activeSectionElements = document.querySelectorAll(".active-section"); - for (let i = 0; i < activeSectionElements.length; i++) { - activeSectionElements[i].classList.remove("active-section") - } - - for (let i = 0; i < activeAnchors.length; i++) { - const headline = activeAnchors[i].parentElement; - const headlineRank = activeAnchors[i].parentElement.nodeName.substr(1); - let el = headline; - - while (el) { - if (el.tagName != "BR" && el.tagName != "HR") { - el.classList.add("active-section"); - } - el = el.nextElementSibling; - - if (el) { - elRank = el.nodeName.substr(1) - - if (elRank > 0 && elRank <= headlineRank) { - break; - } - } - } - } - - for (let i = 0; i < detailsElements.length; i++) { - let detailsElement = detailsElements[i]; - - detailsElement.classList.remove("active"); - } - - if (activeAnchors.length > 0) { - for (let i = 0; i < activeAnchors.length; i++) { - let element = activeAnchors[i]; - - for ( ; element && element !== document; element = element.parentElement ) { - if (element.tagName == "DETAILS") { - element.classList.add("active"); - - if (!doNotOpen) { - element.open = true; - } - } - } - } - } -}; - -const highlightActiveOnPageLink = function() { - var activeHash; - - if (firstCall) { - firstCall = false; - - if (location.hash.length > 0) { - activeHash = location.hash.substr(1); - - highlightDetailsOnActiveHash(activeHash); - } - window.addEventListener('scroll', highlightActiveOnPageLink); - } - - setTimeout(function() { - if (!activeHash) { - const anchors = document.querySelectorAll("h2 > .anchor, h3 > .anchor"); - - if (anchors.length > 0) { - if (document.scrollingElement.scrollTop < 100) { - activeHash = anchors[0].attributes.id.value; - } else if (document.scrollingElement.scrollTop == document.scrollingElement.scrollHeight - document.scrollingElement.clientHeight) { - activeHash = anchors[anchors.length - 1].attributes.id.value; - } else { - for (let i = 0; i < anchors.length; i++) { - const anchor = anchors[i]; - - if (anchor.parentElement.getBoundingClientRect().top < window.screen.availHeight*0.5) { - activeHash = anchor.attributes.id.value; - } else { - break; - } - } - } - } - - if (!activeHash) { - const firstOnPageNavLink = document.querySelectorAll(".toc-headings:first-child > li:first-child > a"); - - if (firstOnPageNavLink.attributes) { - activeHash = firstOnPageNavLink.attributes.href.value.substr(1); - } - } - } - - const allLinks = document.querySelectorAll("a"); - - for (let i = 0; i < allLinks.length; i++) { - const link = allLinks[i]; - link.classList.remove("active"); - - if (link.parentElement && link.parentElement.parentElement && link.parentElement.parentElement.tagName == "UL") { - link.parentElement.parentElement.classList.remove("active") - } - } - - const activeLinks = document.querySelectorAll("a[href='#" + activeHash + "'"); - - for (let i = 0; i < activeLinks.length; i++) { - const link = activeLinks[i]; - link.classList.add("active"); - - if (link.parentElement && link.parentElement.parentElement && link.parentElement.parentElement.tagName == "UL") { - link.parentElement.parentElement.classList.add("active") - } - } - }, 100) -}; - -const hashLinkClickSet = false; - -const allowHashLinkClick = function() { - if (!hashLinkClickSet) { - const hashLinkIcons = document.querySelectorAll(".hash-link-icon"); - - for (let i = 0; i < hashLinkIcons.length; i++) { - const hashLinkIcon = hashLinkIcons[i]; - hashLinkIcon.addEventListener("mousedown", function() { - history.pushState(null, null, hashLinkIcon.parentElement.attributes.href.value); - highlightActiveOnPageLink(); - highlightDetailsOnActiveHash(location.hash.substr(1), true); - }); - } - } -}; - -window.addEventListener('load', allowHashLinkClick); -window.addEventListener('load', highlightActiveOnPageLink); -window.addEventListener('popstate', function (event) { - highlightDetailsOnActiveHash(location.hash.substr(1)); -}, false); -window.addEventListener("click", function (e) { - if (e.target.nodeName == "A" || e.target == document) { - setTimeout(function() { - highlightDetailsOnActiveHash(location.hash.substr(1)); - }, 1000); - } -}); - -/* -const fixCopyButtons = function(e){ - if (e.target.nodeName == "A" || e.target == document) { - setTimeout(function() { - document.querySelectorAll('html body .markdown button[aria-label="Copy code to clipboard"]').forEach(function(el) { - const newEl = el.cloneNode(true); - el.parentNode.replaceChild(newEl, el); - newEl.addEventListener("click", function(e) { - const selection = window.getSelection(); - const range = document.createRange(); - range.selectNodeContents(e.target.parentElement.querySelector(':scope > .prism-code')); - selection.removeAllRanges(); - selection.addRange(range); - - document.execCommand('copy'); - selection.removeAllRanges(); - - const original = e.target.textContent; - e.target.textContent = 'Copied'; - - setTimeout(() => { - e.target.textContent = original; - }, 1200); - }) - }); - }, 1000); - } -} - -document.addEventListener("DOMContentLoaded", fixCopyButtons); -window.addEventListener("popstate", fixCopyButtons); -window.addEventListener("click", fixCopyButtons);*/