diff --git a/docs/.vitepress/devui-theme/components/Page.vue b/docs/.vitepress/devui-theme/components/Page.vue index 2aee748b10c2f02e1e770599e4fe68baca22d639..cb29a342e04da2ff6e3eafeb152088341a40a733 100644 --- a/docs/.vitepress/devui-theme/components/Page.vue +++ b/docs/.vitepress/devui-theme/components/Page.vue @@ -27,6 +27,7 @@ import PageToc from "./PageToc.vue" @media (min-width: 720px) { .page { margin-left: 16.4rem; + margin-right: 40px; } } diff --git a/docs/.vitepress/devui-theme/components/PageToc.vue b/docs/.vitepress/devui-theme/components/PageToc.vue index 5efcd8af45d8b7c193e7df56cb871e37d53ec935..d1666e33113c73d4b722ba372eb1648977074e37 100644 --- a/docs/.vitepress/devui-theme/components/PageToc.vue +++ b/docs/.vitepress/devui-theme/components/PageToc.vue @@ -1,22 +1,29 @@ @@ -27,7 +34,7 @@ const headers = useToc() .devui-content-nav { width: 240px; position: fixed; - top: 90px; + top: 50px; right: 0; height: 100%; z-index: 1; @@ -73,14 +80,14 @@ const headers = useToc() a.current { color: $devui-link; } - - &.active { - color: $devui-link; - } } } } +.active { + color: $devui-link !important; +} + @media (max-width: 1800px) { .devui-content-nav { width: 150px; diff --git a/docs/.vitepress/devui-theme/composables/activeBar.ts b/docs/.vitepress/devui-theme/composables/activeBar.ts new file mode 100644 index 0000000000000000000000000000000000000000..b8d10d5c101c8c003423980b96e25f9899cddce6 --- /dev/null +++ b/docs/.vitepress/devui-theme/composables/activeBar.ts @@ -0,0 +1,136 @@ +import { onMounted, onUnmounted, onUpdated } from 'vue' + +import type { Ref } from 'vue' + +// 防抖节流控制 +export const throttleAndDebounce = (fn: () => any, delay: number) => { + let timeout: ReturnType + let called = false + return () => { + if (timeout) { + clearTimeout(timeout) + } + if (!called) { + fn() + called = true + setTimeout(() => { + called = false + }, delay) + } else { + timeout = setTimeout(fn, delay) + } + } +} + +export function useActiveSidebarLinks( + container: Ref, + marker: Ref +) { + const onScroll = throttleAndDebounce(setActiveLink, 150) + function setActiveLink() { + const sidebarLinks = getSidebarLinks() + const anchors = getAnchors(sidebarLinks) + + if ( + anchors.length && + window.scrollY + window.innerHeight === document.body.offsetHeight + ) { + activateLink(anchors[anchors.length - 1].hash) + return + } + for (let i = 0; i < anchors.length; i++) { + const anchor = anchors[i] + const nextAnchor = anchors[i + 1] + const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor) + if (isActive) { + history.replaceState( + null, + document.title, + hash ? (hash as string) : ' ' + ) + activateLink(hash as string) + return + } + } + } + + let prevActiveLink: HTMLAnchorElement | null = null + + function activateLink(hash: string) { + deactiveLink(prevActiveLink) + + const activeLink = (prevActiveLink = + hash == null + ? null + : (container.value.querySelector( + `.devui-item a[href="${decodeURIComponent(hash)}"]` + ) as HTMLAnchorElement)) + if (activeLink) { + activeLink.classList.add('active') + marker.value.style.opacity = '1' + marker.value.style.top = `${activeLink.offsetTop}px` + } else { + marker.value.style.opacity = '0' + marker.value.style.top = '33px' + } + } + + function deactiveLink(link: HTMLElement) { + link && link.classList.remove('active') + } + + onMounted(() => { + window.requestAnimationFrame(setActiveLink) + window.addEventListener('scroll', onScroll) + }) + + onUpdated(() => { + activateLink(location.hash) + }) + + onUnmounted(() => { + window.removeEventListener('scroll', onScroll) + }) +} +function getSidebarLinks() { + return Array.from( + document.querySelectorAll('.devui-content-nav .devui-link') + ) as HTMLAnchorElement[] +} +function getAnchors(sidebarLinks: HTMLAnchorElement[]) { + return ( + Array.from( + document.querySelectorAll('.content .header-anchor') + ) as HTMLAnchorElement[] + ).filter((anchor) => + sidebarLinks.some((sidebarLink) => sidebarLink.hash === anchor.hash) + ) +} +function getPageOffset() { + return (document.querySelector('.nav-bar') as HTMLElement).offsetHeight +} +function getAnchorTop(anchor: HTMLAnchorElement) { + const pageOffset = getPageOffset() + try { + return anchor.parentElement.offsetTop - pageOffset - 15 + } catch (e) { + return 0 + } +} +function isAnchorActive( + index: number, + anchor: HTMLAnchorElement, + nextAnchor: HTMLAnchorElement +) { + 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, decodeURIComponent(anchor.hash)] + } + return [false, null] +}