Skip to content

Commit 3e0c815

Browse files
committed
feat: to top
Signed-off-by: Innei <[email protected]>
1 parent f413e23 commit 3e0c815

File tree

7 files changed

+45
-22
lines changed

7 files changed

+45
-22
lines changed

src/app/notes/[id]/page.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { MarkdownImageRecordProvider } from '~/providers/article/markdown-image-
2525
import { useSetCurrentNoteId } from '~/providers/note/current-note-id-provider'
2626
import { NoteLayoutRightSidePortal } from '~/providers/note/right-side-provider'
2727
import { parseDate } from '~/utils/datetime'
28+
import { springScrollToTop } from '~/utils/scroller'
2829

2930
import styles from './page.module.css'
3031

@@ -162,5 +163,9 @@ const Markdownrenderers: { [name: string]: Partial<MarkdownToJSX.Rule> } = {
162163

163164
export default PageDataHolder(PageImpl, () => {
164165
const { id } = useParams() as { id: string }
166+
167+
useEffect(() => {
168+
springScrollToTop()
169+
}, [id])
165170
return useNoteByNidQuery(id)
166171
})

src/components/ui/fab/BackToTopFAB.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useViewport } from '~/atoms'
44
import { usePageScrollLocationSelector } from '~/providers/root/page-scroll-info-provider'
5-
import { springScrollToTop } from '~/utils/spring'
5+
import { springScrollToTop } from '~/utils/scroller'
66

77
import { FABBase } from './FABContainer'
88

src/components/widgets/toc/Toc.tsx

+22-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { memo, useEffect, useMemo, useRef, useState } from 'react'
1+
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
22
import { motion } from 'framer-motion'
3+
import { atom, useAtom } from 'jotai'
34
import type { ITocItem } from './TocItem'
45

56
import { RightToLeftTransitionView } from '~/components/ui/transition/RightToLeftTransitionView'
67
import { throttle } from '~/lib/_'
78
import { useArticleElement } from '~/providers/article/article-element-provider'
89
import { clsxm } from '~/utils/helper'
10+
import { springScrollToElement } from '~/utils/scroller'
911

1012
import { TocItem } from './TocItem'
1113

@@ -28,6 +30,7 @@ export const Toc: Component<TocProps> = ({ useAsWeight, className }) => {
2830
...$article.querySelectorAll('h1,h2,h3,h4,h5,h6'),
2931
] as HTMLHeadingElement[]
3032
}, [$article])
33+
3134
const toc: ITocItem[] = useMemo(() => {
3235
return Array.from($headings).map((el, idx) => {
3336
const depth = +el.tagName.slice(1)
@@ -76,8 +79,18 @@ export const Toc: Component<TocProps> = ({ useAsWeight, className }) => {
7679
[toc],
7780
)
7881

79-
const activeId = useActiveId($headings)
82+
const [activeId, setActiveId] = useActiveId($headings)
8083

84+
const handleScrollTo = useCallback(
85+
(i: number, $el: HTMLElement | null, anchorId: string) => {
86+
if ($el) {
87+
springScrollToElement($el, -100).then(() => {
88+
setActiveId(anchorId)
89+
})
90+
}
91+
},
92+
[],
93+
)
8194
return (
8295
<aside className={clsxm('st-toc z-[3]', 'relative font-sans', className)}>
8396
<ul
@@ -92,6 +105,7 @@ export const Toc: Component<TocProps> = ({ useAsWeight, className }) => {
92105
isActive={heading.anchorId === activeId}
93106
key={heading.title}
94107
rootDepth={rootDepth}
108+
onClick={handleScrollTo}
95109
/>
96110
)
97111
})}
@@ -104,7 +118,7 @@ const MemoedItem = memo<{
104118
isActive: boolean
105119
heading: ITocItem
106120
rootDepth: number
107-
onClick?: (i: number) => void
121+
onClick?: (i: number, $el: HTMLElement | null, anchorId: string) => void
108122
// containerRef: any
109123
}>((props) => {
110124
const {
@@ -151,8 +165,10 @@ const MemoedItem = memo<{
151165

152166
MemoedItem.displayName = 'MemoedItem'
153167

168+
const tocActiveIdAtom = atom<string | null>(null)
169+
154170
function useActiveId($headings: HTMLHeadingElement[]) {
155-
const [activeId, setActiveId] = useState<string | null>()
171+
const [activeId, setActiveId] = useAtom(tocActiveIdAtom)
156172
useEffect(() => {
157173
const observer = new IntersectionObserver(
158174
(entries) => {
@@ -177,5 +193,6 @@ function useActiveId($headings: HTMLHeadingElement[]) {
177193
observer.disconnect()
178194
}
179195
}, [$headings])
180-
return activeId
196+
197+
return [activeId, setActiveId] as const
181198
}

src/components/widgets/toc/TocAutoScroll.tsx

+2-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { useEffect } from 'react'
22

33
import { useArticleElement } from '~/providers/article/article-element-provider'
44

5+
import { escapeSelector } from './escapeSelector'
6+
57
export const TocAutoScroll: Component = () => {
68
const articleElement = useArticleElement()
79

@@ -22,7 +24,3 @@ export const TocAutoScroll: Component = () => {
2224

2325
return null
2426
}
25-
26-
function escapeSelector(selector: string) {
27-
return selector.replace(/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, '\\$&')
28-
}

src/components/widgets/toc/TocItem.tsx

+5-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import type { FC, MouseEvent } from 'react'
44

55
import { useArticleElement } from '~/providers/article/article-element-provider'
66
import { clsxm } from '~/utils/helper'
7-
import { springScrollToElement } from '~/utils/spring'
7+
8+
import { escapeSelector } from './escapeSelector'
89

910
const styles = tv({
1011
base: clsxm(
@@ -30,7 +31,7 @@ export const TocItem: FC<{
3031
depth: number
3132
active: boolean
3233
rootDepth: number
33-
onClick?: (i: number) => void
34+
onClick?: (i: number, $el: HTMLElement | null, anchorId: string) => void
3435
index: number
3536
// containerRef?: RefObject<HTMLDivElement>
3637
}> = memo((props) => {
@@ -71,14 +72,11 @@ export const TocItem: FC<{
7172
onClick={useCallback(
7273
(e: MouseEvent) => {
7374
e.preventDefault()
74-
onClick?.(index)
7575
const $el = $article?.querySelector(
76-
`#${anchorId}`,
76+
`#${escapeSelector(anchorId)}`,
7777
) as any as HTMLElement
7878

79-
if ($el) {
80-
springScrollToElement($el, -100)
81-
}
79+
onClick?.(index, $el, anchorId)
8280
},
8381
[onClick, index, $article, anchorId],
8482
)}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function escapeSelector(selector: string) {
2+
return selector.replace(/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, '\\$&')
3+
}

src/utils/spring.ts renamed to src/utils/scroller.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,31 @@ import { microdampingPreset } from '~/constants/spring'
77
export const springScrollTo = (y: number) => {
88
const scrollTop =
99
// FIXME latest version framer will ignore keyframes value `0`
10-
(document.documentElement.scrollTop || document.body.scrollTop) + 1
10+
document.documentElement.scrollTop || document.body.scrollTop
1111
const animation = animateValue({
12-
keyframes: [scrollTop, y],
12+
keyframes: [scrollTop + 1, y],
1313
autoplay: true,
1414
...microdampingPreset,
15+
1516
onUpdate(latest) {
16-
console.log(latest, 'latest')
1717
if (latest <= 0) {
1818
animation.stop()
1919
}
2020
window.scrollTo(0, latest)
2121
},
2222
})
23+
return animation
2324
}
25+
2426
export const springScrollToTop = () => {
25-
springScrollTo(0)
27+
return springScrollTo(0)
2628
}
2729

2830
export const springScrollToElement = (element: HTMLElement, delta = 40) => {
2931
const y = calculateElementTop(element)
3032

3133
const to = y + delta
32-
springScrollTo(to)
34+
return springScrollTo(to)
3335
}
3436

3537
const calculateElementTop = (el: HTMLElement) => {

0 commit comments

Comments
 (0)