Skip to content

Commit

Permalink
refactor(theme): improve robustness and readability of outline compon…
Browse files Browse the repository at this point in the history
…ent (#3368)
  • Loading branch information
zhangyx1998 committed Jan 3, 2024
1 parent 017395f commit 3654470
Showing 1 changed file with 59 additions and 46 deletions.
105 changes: 59 additions & 46 deletions src/client/theme-default/composables/outline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import type { Header } from '../../shared'
import { useAside } from './aside'
import { throttleAndDebounce } from '../support/utils'

// magic number to avoid repeated retrieval
const PAGE_OFFSET = 71
// cached list of anchor elements from resolveHeaders
const resolvedHeaders: { element: HTMLHeadElement; link: string }[] = []

export type MenuItem = Omit<Header, 'slug' | 'children'> & {
element: HTMLHeadElement
children?: MenuItem[]
}

Expand All @@ -29,6 +30,7 @@ export function getHeaders(range: DefaultTheme.Config['outline']) {
.map((el) => {
const level = Number(el.tagName[1])
return {
element: el as HTMLHeadElement,
title: serializeHeader(el),
link: '#' + el.id,
level
Expand Down Expand Up @@ -78,6 +80,12 @@ export function resolveHeaders(
: levelsRange

headers = headers.filter((h) => h.level >= high && h.level <= low)
// clear previous caches
resolvedHeaders.length = 0
// update global header list for active link rendering
for (const { element, link } of headers) {
resolvedHeaders.push({ element, link })
}

const ret: MenuItem[] = []
outer: for (let i = 0; i < headers.length; i++) {
Expand Down Expand Up @@ -128,40 +136,55 @@ export function useActiveAnchor(
return
}

const links = [].slice.call(
container.value.querySelectorAll('.outline-link')
) as HTMLAnchorElement[]

const anchors = [].slice
.call(document.querySelectorAll('.content .header-anchor'))
.filter((anchor: HTMLAnchorElement) => {
return links.some((link) => {
return link.hash === anchor.hash && anchor.offsetParent !== null
})
}) as HTMLAnchorElement[]
// pixel offset, start of main content
const offsetDocTop = (() => {
const container =
document.querySelector('#VPContent .VPDoc')?.firstElementChild
if (container) return getAbsoluteTop(container as HTMLElement)
else return 78
})()

const scrollY = window.scrollY
const innerHeight = window.innerHeight
const offsetHeight = document.body.offsetHeight
const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1

// page bottom - highlight last one
if (anchors.length && isBottom) {
activateLink(anchors[anchors.length - 1].hash)
// resolvedHeaders may be repositioned, hidden or fix positioned
const headers = resolvedHeaders
.map(({ element, link }) => ({
link,
top: getAbsoluteTop(element)
}))
.filter(({ top }) => !Number.isNaN(top))
.sort((a, b) => a.top - b.top)

// no headers available for active link
if (!headers.length) {
activateLink(null)
return
}

for (let i = 0; i < anchors.length; i++) {
const anchor = anchors[i]
const nextAnchor = anchors[i + 1]
// page top
if (scrollY < 1) {
activateLink(null)
return
}

const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor)
// page bottom - highlight last link
if (isBottom) {
activateLink(headers[headers.length - 1].link)
return
}

if (isActive) {
activateLink(hash)
return
// find the last header above the top of viewport
let activeLink: string | null = null
for (const { link, top } of headers) {
if (top > scrollY + offsetDocTop) {
break
}
activeLink = link
}
activateLink(activeLink)
}

function activateLink(hash: string | null) {
Expand Down Expand Up @@ -190,28 +213,18 @@ export function useActiveAnchor(
}
}

function getAnchorTop(anchor: HTMLAnchorElement): number {
return anchor.parentElement!.offsetTop - PAGE_OFFSET
}

function isAnchorActive(
index: number,
anchor: HTMLAnchorElement,
nextAnchor: HTMLAnchorElement | undefined
): [boolean, string | null] {
const scrollTop = window.scrollY

if (index === 0 && scrollTop === 0) {
return [true, null]
}

if (scrollTop < getAnchorTop(anchor)) {
return [false, null]
}

if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)) {
return [true, anchor.hash]
function getAbsoluteTop(element: HTMLElement): number {
let offsetTop = 0
while (element !== document.body) {
if (element === null) {
// child element is:
// - not attached to the DOM (display: none)
// - set to fixed position (not scrollable)
// - body or html element (null offsetParent)
return NaN
}
offsetTop += element.offsetTop
element = element.offsetParent as HTMLElement
}

return [false, null]
return offsetTop

This comment was marked as spam.

Copy link
@sackk11

sackk11 Jan 3, 2024

sasasas

}

0 comments on commit 3654470

Please sign in to comment.