feat: active sidebar links

pull/27/head
Evan You 5 years ago
parent b1537a78ba
commit d2ea9637ee

@ -12,7 +12,7 @@ export function useSiteData() {
// hmr // hmr
if (import.meta.hot) { if (import.meta.hot) {
import.meta.hot.acceptDeps('/@siteData', (m) => { import.meta.hot!.acceptDeps('/@siteData', (m) => {
siteDataRef.value = parse(m.default) siteDataRef.value = parse(m.default)
}) })
} }

@ -22,7 +22,7 @@ export function createApp() {
if (import.meta.hot) { if (import.meta.hot) {
// hot reload pageData // hot reload pageData
import.meta.hot.on('vitepress:pageData', (data) => { import.meta.hot!.on('vitepress:pageData', (data) => {
if ( if (
data.path.replace(/(\bindex)?\.md$/, '') === data.path.replace(/(\bindex)?\.md$/, '') ===
location.pathname.replace(/(\bindex)?\.html$/, '') location.pathname.replace(/(\bindex)?\.html$/, '')

@ -1,4 +1,4 @@
import { reactive, inject, markRaw } from 'vue' import { reactive, inject, markRaw, nextTick } from 'vue'
import type { Component, InjectionKey } from 'vue' import type { Component, InjectionKey } from 'vue'
export interface Route { export interface Route {
@ -61,7 +61,7 @@ export function createRouter(
} }
route.contentComponent = markRaw(comp) route.contentComponent = markRaw(comp)
if (inBrowser) { if (inBrowser) {
setTimeout(() => { nextTick(() => {
if (targetLoc.hash && !scrollPosition) { if (targetLoc.hash && !scrollPosition) {
const target = document.querySelector( const target = document.querySelector(
targetLoc.hash targetLoc.hash

@ -2,6 +2,7 @@ import { useSiteData, usePageData, useRoute } from 'vitepress'
import { computed, h, FunctionalComponent } from 'vue' import { computed, h, FunctionalComponent } from 'vue'
import { Header } from '../../../../types/shared' import { Header } from '../../../../types/shared'
import { DefaultTheme } from '../config' import { DefaultTheme } from '../config'
import { useActiveSidebarLinks } from '../composables/activeSidebarLink'
const SideBarItem: FunctionalComponent<{ const SideBarItem: FunctionalComponent<{
item: ResolvedSidebarItem item: ResolvedSidebarItem
@ -30,6 +31,8 @@ export default {
const siteData = useSiteData() const siteData = useSiteData()
const route = useRoute() const route = useRoute()
useActiveSidebarLinks()
const resolveSidebar = () => { const resolveSidebar = () => {
const { const {
headers, headers,

@ -12,21 +12,31 @@
.sidebar ul { .sidebar ul {
list-style-type: none; list-style-type: none;
line-height: 2; line-height: 2;
padding-left: 1.5rem; padding: 0;
margin: 0; margin: 0;
font-weight: 500;
} }
.sidebar a { .sidebar a {
display: inline-block;
color: var(--text-color); color: var(--text-color);
padding-left: 1.5rem;
} }
.sidebar a:hover { .sidebar a:hover {
color: var(--accent-color); color: var(--accent-color);
} }
.sidebar a.active {
color: var(--accent-color);
font-weight: 500;
}
.sidebar > ul > li > a.active {
padding-left: 1.25rem;
border-left: .25rem solid var(--accent-color);
}
.sidebar ul ul { .sidebar ul ul {
font-weight: 400;
font-size: 0.9em; font-size: 0.9em;
padding-left: 1rem; padding-left: 1rem;
} }

@ -0,0 +1,84 @@
import { onMounted, onUnmounted, onUpdated } from 'vue'
export function useActiveSidebarLinks() {
let rootActiveLink: HTMLAnchorElement | null = null
let activeLink: HTMLAnchorElement | null = null
const decode = decodeURIComponent
const deactiveLink = (link: HTMLAnchorElement | null) =>
link && link.classList.remove('active')
const activateLink = (hash: string) => {
deactiveLink(activeLink)
deactiveLink(rootActiveLink)
activeLink = document.querySelector(`.sidebar a[href="${hash}"]`)
if (activeLink) {
activeLink.classList.add('active')
// also add active class to parent h2 anchors
const rootLi = activeLink.closest('.sidebar > ul > li')
if (rootLi && rootLi !== activeLink.parentElement) {
rootActiveLink = rootLi.querySelector('a')
rootActiveLink && rootActiveLink.classList.add('active')
} else {
rootActiveLink = null
}
}
}
const setActiveLink = () => {
const sidebarLinks = [].slice.call(
document.querySelectorAll('.sidebar a')
) as HTMLAnchorElement[]
const anchors = [].slice
.call(document.querySelectorAll('.header-anchor'))
.filter((anchor: HTMLAnchorElement) =>
sidebarLinks.some((sidebarLink) => sidebarLink.hash === anchor.hash)
) as HTMLAnchorElement[]
const pageOffset = document.getElementById('app')!.offsetTop
const scrollTop = window.scrollY
const getAnchorTop = (anchor: HTMLAnchorElement): number =>
anchor.parentElement!.offsetTop - pageOffset - 15
for (let i = 0; i < anchors.length; i++) {
const anchor = anchors[i]
const nextAnchor = anchors[i + 1]
const isActive =
(i === 0 && scrollTop === 0) ||
(scrollTop >= getAnchorTop(anchor) &&
(!nextAnchor || scrollTop < getAnchorTop(nextAnchor)))
if (isActive) {
const targetHash = decode(anchor.hash)
history.replaceState(null, document.title, targetHash)
activateLink(targetHash)
return
}
}
}
const onScroll = debounce(setActiveLink, 100)
onMounted(() => {
setActiveLink()
window.addEventListener('scroll', onScroll)
})
onUpdated(() => {
// sidebar update means a route change
activateLink(decode(location.hash))
})
onUnmounted(() => {
window.removeEventListener('scroll', onScroll)
})
}
function debounce(fn: () => void, delay: number): () => void {
let timeout: number
return () => {
if (timeout) clearTimeout(timeout)
timeout = setTimeout(fn, delay)
}
}

@ -35,7 +35,7 @@ aside {
left: 0; left: 0;
height: 100%; height: 100%;
width: var(--sidebar-width); width: var(--sidebar-width);
padding: calc(var(--header-height) + 1.5rem) 1.5rem 1.5rem 0; padding: calc(var(--header-height) + 1.5rem) 0 1.5rem 0;
border-right: 1px solid var(--border-color); border-right: 1px solid var(--border-color);
background-color: #fff; background-color: #fff;
z-index: 3; z-index: 3;

Loading…
Cancel
Save