From 8b3602aecb1efe020ca7f9601da9c4b3d1851b29 Mon Sep 17 00:00:00 2001 From: jaywcjlove <398188662@qq.com> Date: Sun, 16 Apr 2023 13:50:53 +0800 Subject: [PATCH] website: fix toc scroll issue. --- website/src/components/Preview/Hyperlink.tsx | 89 ++++++++++++++ website/src/components/Preview/index.tsx | 4 +- .../src/components/Preview/useHyperlink.tsx | 113 ------------------ 3 files changed, 91 insertions(+), 115 deletions(-) create mode 100644 website/src/components/Preview/Hyperlink.tsx delete mode 100644 website/src/components/Preview/useHyperlink.tsx diff --git a/website/src/components/Preview/Hyperlink.tsx b/website/src/components/Preview/Hyperlink.tsx new file mode 100644 index 000000000..6bfa20afd --- /dev/null +++ b/website/src/components/Preview/Hyperlink.tsx @@ -0,0 +1,89 @@ +import { useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +export function debounce(func: (...args: T) => void, wait: number) { + let timeout: ReturnType; + return function (...args: T) { + // @ts-ignore + const context = this; + clearTimeout(timeout); + timeout = setTimeout(() => { + func.apply(context, args); + }, wait); + }; +} + +function scrollTop(offsetTop: number) { + document.documentElement.scrollTo(0, offsetTop); +} + +export const Hyperlink = (props: any) => { + const { dom } = props; + let [searchParams, setSearchParams] = useSearchParams(); + useEffect(() => { + if (dom) { + const clickFn = (event: MouseEvent) => { + event.preventDefault(); + const target = event.currentTarget as HTMLAnchorElement; + const hash = new URL(target.href || '').hash.replace(/^#/, ''); + const elm = document.getElementById(decodeURIComponent(hash)) as HTMLHeadingElement; + scrollTop(elm.offsetTop); + searchParams.set('_id', decodeURIComponent(hash)); + setSearchParams(searchParams); + if (target.className.indexOf('toc-link') > -1) { + (target.parentElement?.childNodes as unknown as HTMLAnchorElement[]).forEach((item) => { + item.classList.remove('active'); + }); + target.classList.add('active'); + } + }; + setTimeout(() => { + const anchor = dom.getElementsByClassName('anchor') as HTMLCollectionOf; + Array.from(anchor).forEach((item) => { + item.onclick = clickFn; + }); + const tocAnchor = dom.getElementsByClassName('toc-link') as HTMLCollectionOf; + Array.from(tocAnchor).forEach((item) => { + item.onclick = clickFn; + }); + }, 900); + } + }, [dom, searchParams, setSearchParams]); + + useEffect(() => { + const scrollEndFn = () => { + const anchor = dom?.getElementsByClassName('anchor') as HTMLCollectionOf; + let hash = ''; + Array.from(anchor || []).forEach((item, idx) => { + if (idx === 0) { + hash = new URL(item.href || '').hash.replace(/^#/, ''); + } + if (document.documentElement.scrollTop - item.offsetTop > -1) { + hash = new URL(item.href || '').hash.replace(/^#/, ''); + } + }); + + const tocLink = document.querySelector(`a[href="#${decodeURIComponent(hash)}"].toc-link`) as HTMLAnchorElement; + if (tocLink) { + (tocLink.parentElement?.childNodes as unknown as HTMLAnchorElement[]).forEach((item) => { + item.classList.remove('active'); + }); + tocLink.classList.add('active'); + searchParams.set('_id', decodeURIComponent(hash)); + setSearchParams(searchParams); + } + const marker = document.querySelector('.menu-marker') as HTMLDivElement; + const tocLinkActive = document.querySelector('.toc-link.active') as HTMLDivElement; + if (tocLinkActive && marker) { + marker.style.top = `${tocLinkActive.offsetTop}px`; + } + }; + const scrollEnd = debounce(scrollEndFn, 50); + window.addEventListener('scroll', scrollEnd); + return () => { + window.removeEventListener('scroll', scrollEnd); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dom]); + return null; +}; diff --git a/website/src/components/Preview/index.tsx b/website/src/components/Preview/index.tsx index 61786210e..2f386dd25 100644 --- a/website/src/components/Preview/index.tsx +++ b/website/src/components/Preview/index.tsx @@ -6,9 +6,9 @@ import BackToUp from '@uiw/react-back-to-top'; import { getMetaId, isMeta, getURLParameters } from 'markdown-react-code-preview-loader'; import CodeLayout from 'react-code-preview-layout'; import { useMdData, MdDataHandle } from './useMdData'; -import { useHyperlink } from './useHyperlink'; import { getTocTree } from './nodes/toc'; import Footer from '../Footer'; +import { Hyperlink } from './Hyperlink'; import './nodes/toc.less'; // @ts-ignore @@ -57,10 +57,10 @@ interface PreviewDocumentProps { const PreviewDocument = ({ path, editor }: PreviewDocumentProps) => { const $dom = useRef(null); const { mdData } = useMdData(path); - useHyperlink($dom.current); const editorUrl = `https://github.com/uiwjs/react-baidu-map/tree/master/${editor}`; return ( + (func: (...args: T) => void, wait: number) { - let timeout: ReturnType; - return function (...args: T) { - // @ts-ignore - const context = this; - clearTimeout(timeout); - timeout = setTimeout(() => { - func.apply(context, args); - }, wait); - }; -} - -function scrollTop(offsetTop: number) { - document.documentElement.scrollTo(0, offsetTop); -} - -export const useHyperlink = (dom: HTMLDivElement | null) => { - let [searchParams, setSearchParams] = useSearchParams(); - useEffect(() => { - if (dom) { - const clickFn = (event: MouseEvent) => { - event.preventDefault(); - const target = event.currentTarget as HTMLAnchorElement; - const hash = new URL(target.href || '').hash.replace(/^#/, ''); - const elm = document.getElementById(decodeURIComponent(hash)) as HTMLHeadingElement; - scrollTop(elm.offsetTop); - searchParams.set('_id', decodeURIComponent(hash)); - setSearchParams(searchParams); - if (target.className.indexOf('toc-link') > -1) { - (target.parentElement?.childNodes as unknown as HTMLAnchorElement[]).forEach((item) => { - item.classList.remove('active'); - }); - target.classList.add('active'); - } - }; - const scrollFn = () => {}; - const scrollEndFn = () => { - const anchor = dom.getElementsByClassName('anchor') as HTMLCollectionOf; - let hash = ''; - Array.from(anchor).forEach((item, idx) => { - if (idx === 0) { - hash = new URL(item.href || '').hash.replace(/^#/, ''); - } - if (document.documentElement.scrollTop - item.offsetTop > -1) { - hash = new URL(item.href || '').hash.replace(/^#/, ''); - } - }); - const tocLink = document.querySelector(`a[href="#${decodeURIComponent(hash)}"].toc-link`) as HTMLAnchorElement; - if (tocLink) { - (tocLink.parentElement?.childNodes as unknown as HTMLAnchorElement[]).forEach((item) => { - item.classList.remove('active'); - }); - tocLink.classList.add('active'); - } - }; - const callback = () => { - const anchor = dom.getElementsByClassName('anchor') as HTMLCollectionOf; - Array.from(anchor).forEach((item) => { - item.onclick = clickFn; - }); - - const tocAnchor = document - .getElementById('menu-toc') - ?.getElementsByClassName('toc-link') as HTMLCollectionOf; - Array.from(tocAnchor || []).forEach((item) => { - item.onclick = clickFn; - }); - - const hash = searchParams.get('_id'); - if (hash) { - const $elm = document.getElementById(hash) as HTMLHeadingElement; - scrollTop($elm.offsetTop); - } - document.documentElement.addEventListener('scroll', scrollFn); - document.documentElement.addEventListener('scroll', debounce(scrollEndFn, 50)); - }; - // 创建一个观察器实例并传入回调函数 - const observer = new MutationObserver(callback); - // 以上述配置开始观察目标节点 - observer.observe(dom, { childList: true, subtree: true }); - - const callbackToc = (mutationList: MutationRecord[], observer: MutationObserver) => { - for (const mutation of mutationList) { - if (mutation.type === 'attributes') { - const target = mutation.target as HTMLAnchorElement; - if ( - target && - target.className && - Array.isArray(target.className) && - target.className.indexOf('toc-link') > -1 - ) { - // console.log('>>>', target); - const marker = document.querySelector('.menu-marker') as HTMLDivElement; - marker.style.top = `${target.offsetTop}px`; - } - } - } - }; - - const observerToc = new MutationObserver(callbackToc); - observerToc.observe(dom, { attributes: true, childList: true, subtree: true }); - return () => { - // 停止观察 - observer.disconnect(); - // 停止观察 - // observerToc.disconnect(); - }; - } - }, [dom, searchParams, setSearchParams]); -};