mirror of
https://github.com/nunocoracao/blowfish.git
synced 2026-10-10 22:09:31 +00:00
getActiveAnchorId derives the active entry from scroll position alone, so on a page short enough that scrollHeight never exceeds window.innerHeight, the answer can never change. Clicking a TOC entry still updates location.hash, but nothing ever picks it up as active. Falls back to the hash when the page can't scroll, honoured only if it names an entry the TOC actually lists. Pages that do scroll are unaffected - this only fires when scroll position can't answer the question at all. Fixes #3087
141 lines
5.3 KiB
HTML
141 lines
5.3 KiB
HTML
<details
|
|
open
|
|
id="TOCView"
|
|
class="toc-right mt-0 overflow-y-auto overscroll-contain bf-scrollbar rounded-lg -ms-5 ps-5 pe-2 hidden lg:block">
|
|
<summary
|
|
class="block py-1 text-lg font-semibold cursor-pointer bg-neutral-100 text-neutral-800 -ms-5 ps-5 dark:bg-neutral-700 dark:text-neutral-100 lg:hidden">
|
|
{{ i18n "article.table_of_contents" }}
|
|
</summary>
|
|
<div
|
|
class="min-w-[220px] py-2 border-dotted border-s-1 -ms-5 ps-5 dark:border-neutral-600">
|
|
{{ replace (.TableOfContents | emojify) `id="TableOfContents"` (printf `id="TableOfContents" aria-label="%s"` (i18n "article.table_of_contents")) | safeHTML }}
|
|
</div>
|
|
</details>
|
|
<details class="toc-inside mt-0 overflow-hidden rounded-lg -ms-5 ps-5 lg:hidden">
|
|
<summary
|
|
class="py-1 text-lg font-semibold cursor-pointer bg-neutral-100 text-neutral-800 -ms-5 ps-5 dark:bg-neutral-700 dark:text-neutral-100 lg:hidden">
|
|
{{ i18n "article.table_of_contents" }}
|
|
</summary>
|
|
<div
|
|
class="py-2 border-dotted border-neutral-300 border-s-1 -ms-5 ps-5 dark:border-neutral-600">
|
|
{{ replace (.TableOfContents | emojify) `id="TableOfContents"` (printf `id="TableOfContentsMobile" aria-label="%s"` (i18n "article.table_of_contents")) | safeHTML }}
|
|
</div>
|
|
</details>
|
|
|
|
{{ if .Site.Params.smartTOC }}
|
|
<script>
|
|
(function () {
|
|
'use strict'
|
|
|
|
const SCROLL_OFFSET_RATIO = 0.33
|
|
const TOC_SELECTOR = '#TableOfContents'
|
|
const ANCHOR_SELECTOR = '.anchor'
|
|
const TOC_LINK_SELECTOR = 'a[href^="#"]'
|
|
const NESTED_LIST_SELECTOR = 'li ul'
|
|
const ACTIVE_CLASS = 'active'
|
|
let isJumpingToAnchor = false
|
|
|
|
function getActiveAnchorId(anchors, offsetRatio) {
|
|
// On a page short enough that it never scrolls, scroll position can
|
|
// never change, so the loop below can never pick a different entry.
|
|
// Clicking a TOC entry still changes the hash, so fall back to that
|
|
// when scrolling can't answer the question at all - honoured only if
|
|
// it names an entry the TOC actually lists, so an unrelated hash
|
|
// doesn't fake a match. Pages that do scroll are unaffected.
|
|
const canScroll =
|
|
document.documentElement.scrollHeight > window.innerHeight + 1
|
|
if (!canScroll) {
|
|
const fromHash = decodeURIComponent(window.location.hash.substring(1))
|
|
const known = [...document.querySelectorAll('#TableOfContents a[href^="#"]')]
|
|
.map(link => decodeURIComponent(link.getAttribute('href').substring(1)))
|
|
if (fromHash && known.includes(fromHash)) return fromHash
|
|
}
|
|
|
|
const threshold = window.scrollY + window.innerHeight * offsetRatio
|
|
const tocLinks = [...document.querySelectorAll('#TableOfContents a[href^="#"]')]
|
|
const tocIds = new Set(tocLinks.map(link => link.getAttribute('href').substring(1)))
|
|
|
|
if (isJumpingToAnchor) {
|
|
for (let i = 0; i < anchors.length; i++) {
|
|
const anchor = anchors[i]
|
|
if (!tocIds.has(anchor.id)) continue
|
|
const top = anchor.getBoundingClientRect().top + window.scrollY
|
|
if (Math.abs(window.scrollY - top) < 100) {
|
|
return anchor.id
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let i = anchors.length - 1; i >= 0; i--) {
|
|
const top = anchors[i].getBoundingClientRect().top + window.scrollY
|
|
if (top <= threshold && tocIds.has(anchors[i].id)) {
|
|
return anchors[i].id
|
|
}
|
|
}
|
|
return anchors.find(anchor => tocIds.has(anchor.id))?.id || ''
|
|
}
|
|
|
|
function updateTOC({ toc, anchors, links, scrollOffset, collapseInactive }) {
|
|
const activeId = getActiveAnchorId(anchors, scrollOffset)
|
|
if (!activeId) return
|
|
|
|
links.forEach(link => {
|
|
const isActive = link.getAttribute('href') === `#${activeId}`
|
|
link.classList.toggle(ACTIVE_CLASS, isActive)
|
|
|
|
if (collapseInactive) {
|
|
const ul = link.closest('li')?.querySelector('ul')
|
|
if (ul) ul.style.display = isActive ? '' : 'none'
|
|
}
|
|
})
|
|
|
|
if (collapseInactive) {
|
|
const activeLink = toc.querySelector(`a[href="#${CSS.escape(activeId)}"]`)
|
|
let el = activeLink
|
|
while (el && el !== toc) {
|
|
if (el.tagName === 'UL') el.style.display = ''
|
|
if (el.tagName === 'LI') el.querySelector('ul')?.style.setProperty('display', '')
|
|
el = el.parentElement
|
|
}
|
|
}
|
|
}
|
|
|
|
function initTOC() {
|
|
const toc = document.querySelector(TOC_SELECTOR)
|
|
if (!toc) return
|
|
|
|
const collapseInactive = {{ if site.Params.smartTOCHideUnfocusedChildren }}true{{ else }}false{{ end }}
|
|
const anchors = [...document.querySelectorAll(ANCHOR_SELECTOR)]
|
|
const links = [...toc.querySelectorAll(TOC_LINK_SELECTOR)]
|
|
|
|
if (collapseInactive) {
|
|
toc.querySelectorAll(NESTED_LIST_SELECTOR).forEach(ul => ul.style.display = 'none')
|
|
}
|
|
|
|
links.forEach(link => {
|
|
link.addEventListener('click', () => {
|
|
isJumpingToAnchor = true
|
|
})
|
|
})
|
|
|
|
const config = {
|
|
toc,
|
|
anchors,
|
|
links,
|
|
scrollOffset: SCROLL_OFFSET_RATIO,
|
|
collapseInactive
|
|
}
|
|
|
|
window.addEventListener('scroll', () => updateTOC(config), { passive: true })
|
|
window.addEventListener('hashchange', () => updateTOC(config), { passive: true })
|
|
|
|
updateTOC(config)
|
|
}
|
|
|
|
document.readyState === 'loading'
|
|
? document.addEventListener('DOMContentLoaded', initTOC)
|
|
: initTOC()
|
|
})()
|
|
</script>
|
|
{{ end }}
|