mirror of
https://github.com/acid-info/logos-press-engine.git
synced 2025-02-23 22:58:08 +00:00
close #28 and refactor toc renders
This commit is contained in:
parent
bc2da5ebac
commit
13859dc3c1
@ -1,4 +1,8 @@
|
|||||||
import { UnbodyImageBlock, UnbodyTextBlock } from '@/lib/unbody/unbody.types'
|
import {
|
||||||
|
TextBlockEnhanced,
|
||||||
|
UnbodyImageBlock,
|
||||||
|
UnbodyTextBlock,
|
||||||
|
} from '@/lib/unbody/unbody.types'
|
||||||
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
|
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
|
||||||
import { ArticleImageBlockWrapper } from './Article.ImageBlockWrapper'
|
import { ArticleImageBlockWrapper } from './Article.ImageBlockWrapper'
|
||||||
import { PostImageRatio } from '../Post/Post'
|
import { PostImageRatio } from '../Post/Post'
|
||||||
@ -9,11 +13,17 @@ import {
|
|||||||
extractIdFromFirstTag,
|
extractIdFromFirstTag,
|
||||||
extractInnerHtml,
|
extractInnerHtml,
|
||||||
} from '@/utils/html.utils'
|
} from '@/utils/html.utils'
|
||||||
|
import { HeadingElementsRef } from '@/utils/ui.utils'
|
||||||
|
import UnbodyDocumentTypeNames = UnbodyGraphQl.UnbodyDocumentTypeNames
|
||||||
|
import { ArticleHeading } from '@/components/Article/Article.Heading'
|
||||||
|
|
||||||
export const RenderArticleBlock = ({
|
export const RenderArticleBlock = ({
|
||||||
block,
|
block,
|
||||||
|
headingElementsRef,
|
||||||
}: {
|
}: {
|
||||||
block: UnbodyImageBlock | UnbodyTextBlock
|
block: UnbodyImageBlock | UnbodyTextBlock
|
||||||
|
activeId: string | null
|
||||||
|
headingElementsRef: HeadingElementsRef
|
||||||
}) => {
|
}) => {
|
||||||
switch (block.__typename) {
|
switch (block.__typename) {
|
||||||
case UnbodyGraphQl.UnbodyDocumentTypeNames.ImageBlock:
|
case UnbodyGraphQl.UnbodyDocumentTypeNames.ImageBlock:
|
||||||
@ -30,17 +40,14 @@ export const RenderArticleBlock = ({
|
|||||||
case 'h3':
|
case 'h3':
|
||||||
case 'h4':
|
case 'h4':
|
||||||
case 'h5':
|
case 'h5':
|
||||||
case 'h6':
|
case 'h6': {
|
||||||
return (
|
return (
|
||||||
<Headline
|
<ArticleHeading
|
||||||
variant={block.tagName as any}
|
block={block}
|
||||||
component={block.tagName as any}
|
headingElementsRef={headingElementsRef}
|
||||||
genericFontFamily="sans-serif"
|
|
||||||
className={extractClassFromFirstTag(block.html) || ''}
|
|
||||||
id={extractIdFromFirstTag(block.html) || ''}
|
|
||||||
dangerouslySetInnerHTML={{ __html: extractInnerHtml(block.html) }}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<Paragraph
|
<Paragraph
|
||||||
@ -58,11 +65,6 @@ export const RenderArticleBlock = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Headline = styled(Typography)`
|
|
||||||
white-space: pre-wrap;
|
|
||||||
margin-top: 24px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Paragraph = styled(Typography)`
|
const Paragraph = styled(Typography)`
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
`
|
`
|
||||||
|
@ -6,16 +6,27 @@ import {
|
|||||||
UnbodyImageBlock,
|
UnbodyImageBlock,
|
||||||
UnbodyTextBlock,
|
UnbodyTextBlock,
|
||||||
} from '@/lib/unbody/unbody.types'
|
} from '@/lib/unbody/unbody.types'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useIntersectionObserver } from '@/utils/ui.utils'
|
||||||
|
import { useArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: GoogleDocEnhanced
|
data: GoogleDocEnhanced
|
||||||
}
|
}
|
||||||
|
|
||||||
const ArticleBlocks = ({ data }: Props) => {
|
const ArticleBlocks = ({ data }: Props) => {
|
||||||
|
const { setTocId, tocId } = useArticleContainerContext()
|
||||||
|
const headingElementsRef = useIntersectionObserver(setTocId)
|
||||||
|
|
||||||
return data.blocks.length ? (
|
return data.blocks.length ? (
|
||||||
<>
|
<>
|
||||||
{getBodyBlocks(data).map((block, idx) => (
|
{getBodyBlocks(data).map((block, idx) => (
|
||||||
<RenderArticleBlock key={'block-' + idx} block={block} />
|
<RenderArticleBlock
|
||||||
|
key={'block-' + idx}
|
||||||
|
block={block}
|
||||||
|
activeId={tocId}
|
||||||
|
headingElementsRef={headingElementsRef}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
) : null
|
) : null
|
||||||
|
48
src/components/Article/Article.Heading.tsx
Normal file
48
src/components/Article/Article.Heading.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { TextBlockEnhanced, UnbodyTextBlock } from '@/lib/unbody/unbody.types'
|
||||||
|
import { HeadingElementsRef } from '@/utils/ui.utils'
|
||||||
|
import {
|
||||||
|
extractClassFromFirstTag,
|
||||||
|
extractIdFromFirstTag,
|
||||||
|
extractInnerHtml,
|
||||||
|
} from '@/utils/html.utils'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import { Typography, TypographyProps } from '@acid-info/lsd-react'
|
||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
|
||||||
|
type Props = PropsWithChildren<{
|
||||||
|
block: TextBlockEnhanced | UnbodyTextBlock
|
||||||
|
headingElementsRef: HeadingElementsRef
|
||||||
|
typographyProps?: TypographyProps
|
||||||
|
}>
|
||||||
|
export const ArticleHeading = ({
|
||||||
|
block,
|
||||||
|
headingElementsRef,
|
||||||
|
typographyProps,
|
||||||
|
children,
|
||||||
|
}: Props) => {
|
||||||
|
const id =
|
||||||
|
extractIdFromFirstTag(block.html) || `${block.tagName}-${block.order}`
|
||||||
|
const refProp = {
|
||||||
|
ref: (ref: any) => {
|
||||||
|
headingElementsRef.current[id] = ref
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className="anchor" id={id} {...refProp} />
|
||||||
|
<Headline
|
||||||
|
variant={block.tagName as any}
|
||||||
|
component={block.tagName as any}
|
||||||
|
genericFontFamily="sans-serif"
|
||||||
|
className={extractClassFromFirstTag(block.html) || ''}
|
||||||
|
dangerouslySetInnerHTML={{ __html: `${extractInnerHtml(block.html)}` }}
|
||||||
|
{...(typographyProps || {})}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Headline = styled(Typography)`
|
||||||
|
white-space: pre-wrap;
|
||||||
|
margin-top: 24px;
|
||||||
|
`
|
@ -5,25 +5,28 @@ import { Typography } from '@acid-info/lsd-react'
|
|||||||
import styles from './Article.module.css'
|
import styles from './Article.module.css'
|
||||||
import { GoogleDocEnhanced } from '@/lib/unbody/unbody.types'
|
import { GoogleDocEnhanced } from '@/lib/unbody/unbody.types'
|
||||||
import { Collapse } from '../Collapse'
|
import { Collapse } from '../Collapse'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
toc: Array<Partial<GoogleDocEnhanced>>
|
toc: UnbodyGraphQl.Fragments.TocItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MobileToc = ({ toc }: Props) => {
|
export const MobileToc = ({ toc }: Props) => {
|
||||||
const { tocIndex, setTocIndex } = useArticleContainerContext()
|
const { tocId, setTocId } = useArticleContainerContext()
|
||||||
|
|
||||||
return toc?.length > 0 ? (
|
return toc?.length > 0 ? (
|
||||||
<Collapse className={styles.mobileToc} label="Contents">
|
<Collapse className={styles.mobileToc} label="Contents">
|
||||||
{toc.map((toc, idx) => (
|
{toc.map((toc, idx) => (
|
||||||
<Content
|
<TocItem
|
||||||
onClick={() => setTocIndex(idx)}
|
href={`${idx === 0 ? '#' : toc.href}`}
|
||||||
active={idx === tocIndex}
|
|
||||||
variant="body3"
|
|
||||||
key={idx}
|
key={idx}
|
||||||
|
active={tocId ? toc.href.substring(1) === tocId : idx === 0}
|
||||||
>
|
>
|
||||||
{toc.title}
|
<Typography variant="label2" genericFontFamily="sans-serif">
|
||||||
</Content>
|
{toc.title}
|
||||||
|
</Typography>
|
||||||
|
</TocItem>
|
||||||
))}
|
))}
|
||||||
</Collapse>
|
</Collapse>
|
||||||
) : null
|
) : null
|
||||||
@ -35,7 +38,7 @@ const CustomTypography = styled(Typography)`
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Content = styled(CustomTypography)<{ active: boolean }>`
|
const TocItem = styled(Link)<{ active: boolean }>`
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
background-color: ${(p) =>
|
background-color: ${(p) =>
|
||||||
p.active
|
p.active
|
||||||
|
@ -7,10 +7,13 @@ import ArticleStats from '../Article.Stats'
|
|||||||
import { Typography } from '@acid-info/lsd-react'
|
import { Typography } from '@acid-info/lsd-react'
|
||||||
import styled from '@emotion/styled'
|
import styled from '@emotion/styled'
|
||||||
import ArticleSummary from './Article.Summary'
|
import ArticleSummary from './Article.Summary'
|
||||||
import { UnbodyGraphQl } from '../../../lib/unbody/unbody-content.types'
|
|
||||||
import { calcReadingTime } from '@/utils/string.utils'
|
import { calcReadingTime } from '@/utils/string.utils'
|
||||||
import { Authors } from '@/components/Authors'
|
import { Authors } from '@/components/Authors'
|
||||||
import { Tags } from '@/components/Tags'
|
import { Tags } from '@/components/Tags'
|
||||||
|
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
|
||||||
|
import { ArticleHeading } from '@/components/Article/Article.Heading'
|
||||||
|
import { useArticleContainerContext } from '@/containers/ArticleContainer.Context'
|
||||||
|
import { useIntersectionObserver } from '@/utils/ui.utils'
|
||||||
|
|
||||||
const ArticleHeader = ({
|
const ArticleHeader = ({
|
||||||
title,
|
title,
|
||||||
@ -22,6 +25,9 @@ const ArticleHeader = ({
|
|||||||
modifiedAt,
|
modifiedAt,
|
||||||
blocks,
|
blocks,
|
||||||
}: GoogleDocEnhanced) => {
|
}: GoogleDocEnhanced) => {
|
||||||
|
const { setTocId, tocId } = useArticleContainerContext()
|
||||||
|
const headingElementsRef = useIntersectionObserver(setTocId)
|
||||||
|
|
||||||
const _thumbnail = useMemo(() => {
|
const _thumbnail = useMemo(() => {
|
||||||
const coverImage = getArticleCover(blocks)
|
const coverImage = getArticleCover(blocks)
|
||||||
if (!coverImage) return null
|
if (!coverImage) return null
|
||||||
@ -51,13 +57,15 @@ const ArticleHeader = ({
|
|||||||
return (
|
return (
|
||||||
<header>
|
<header>
|
||||||
<ArticleStats dateStr={modifiedAt} readingLength={readingTime} />
|
<ArticleStats dateStr={modifiedAt} readingLength={readingTime} />
|
||||||
<ArticleTitle
|
<ArticleHeading
|
||||||
id={toc[0].href.substring(1)}
|
block={blocks[0] as any}
|
||||||
variant={'h1'}
|
typographyProps={{
|
||||||
genericFontFamily="serif"
|
variant: 'h1',
|
||||||
>
|
genericFontFamily: 'serif',
|
||||||
{title}
|
component: 'h1',
|
||||||
</ArticleTitle>
|
}}
|
||||||
|
headingElementsRef={headingElementsRef}
|
||||||
|
/>
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
<ArticleSubtitle
|
<ArticleSubtitle
|
||||||
variant="body1"
|
variant="body1"
|
||||||
|
@ -21,7 +21,6 @@ export const ProgressBar = () => {
|
|||||||
<NextNProgress
|
<NextNProgress
|
||||||
color={color}
|
color={color}
|
||||||
height={1}
|
height={1}
|
||||||
showOnShallow={true}
|
|
||||||
options={{
|
options={{
|
||||||
showSpinner: false,
|
showSpinner: false,
|
||||||
}}
|
}}
|
||||||
|
@ -3,36 +3,39 @@ import { useArticleContainerContext } from '@/containers/ArticleContainer.Contex
|
|||||||
import { useSticky } from '@/utils/ui.utils'
|
import { useSticky } from '@/utils/ui.utils'
|
||||||
import { Typography } from '@acid-info/lsd-react'
|
import { Typography } from '@acid-info/lsd-react'
|
||||||
import styled from '@emotion/styled'
|
import styled from '@emotion/styled'
|
||||||
import { UnbodyGoogleDoc } from '@/lib/unbody/unbody.types'
|
|
||||||
import { useArticleContext } from '@/context/article.context'
|
|
||||||
import { useSearchBarContext } from '@/context/searchbar.context'
|
import { useSearchBarContext } from '@/context/searchbar.context'
|
||||||
|
import Link from 'next/link'
|
||||||
export type TableOfContentsProps = Pick<UnbodyGoogleDoc, 'toc'>
|
import { useRouter } from 'next/router'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { UnbodyGraphQl } from '@/lib/unbody/unbody-content.types'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
contents?: TableOfContentsProps['toc']
|
contents?: UnbodyGraphQl.Fragments.TocItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TableOfContents({ contents, ...props }: Props) {
|
export default function TableOfContents({ contents, ...props }: Props) {
|
||||||
const articleContainer = useArticleContainerContext()
|
const { tocId, setTocId } = useArticleContainerContext()
|
||||||
const { tocIndex, setTocIndex } = articleContainer
|
|
||||||
const dy = uiConfigs.navbarRenderedHeight + uiConfigs.postSectionMargin
|
const dy = uiConfigs.navbarRenderedHeight + uiConfigs.postSectionMargin
|
||||||
const { resultsNumber } = useSearchBarContext()
|
const { resultsNumber } = useSearchBarContext()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const { sticky, stickyRef, height } = useSticky<HTMLDivElement>(dy)
|
const { sticky, stickyRef, height } = useSticky<HTMLDivElement>(dy)
|
||||||
|
|
||||||
const handleSectionClick = (index: number) => {
|
useEffect(() => {
|
||||||
//@ts-ignore
|
const onHashChangeStart = (url: string) => {
|
||||||
const section = document.getElementById(contents[index].href.substring(1))
|
const hash = url.split('#')[1]
|
||||||
|
console.log('hash', contents)
|
||||||
const position = section?.getBoundingClientRect()
|
if (hash) {
|
||||||
|
setTocId(hash)
|
||||||
window.scrollTo({
|
} else {
|
||||||
top: Number(position?.top) + window.scrollY - 100,
|
setTocId(null)
|
||||||
behavior: 'smooth',
|
}
|
||||||
})
|
}
|
||||||
setTocIndex(index)
|
router.events.on('hashChangeStart', onHashChangeStart)
|
||||||
}
|
return () => {
|
||||||
|
router.events.off('hashChangeStart', onHashChangeStart)
|
||||||
|
}
|
||||||
|
}, [router.events])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
@ -46,17 +49,16 @@ export default function TableOfContents({ contents, ...props }: Props) {
|
|||||||
>
|
>
|
||||||
<Title variant="body3">Contents</Title>
|
<Title variant="body3">Contents</Title>
|
||||||
<Contents height={height}>
|
<Contents height={height}>
|
||||||
{/* @ts-ignore */}
|
|
||||||
{contents?.map((content, index) => (
|
{contents?.map((content, index) => (
|
||||||
<Section
|
<TocItem
|
||||||
active={index === tocIndex}
|
href={`${index === 0 ? '#' : content.href}`}
|
||||||
onClick={() => handleSectionClick(index)}
|
|
||||||
key={index}
|
key={index}
|
||||||
|
active={tocId ? content.href.substring(1) === tocId : index === 0}
|
||||||
>
|
>
|
||||||
<Typography variant="body3" genericFontFamily="sans-serif">
|
<Typography variant="label2" genericFontFamily="sans-serif">
|
||||||
{content.title}
|
{content.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Section>
|
</TocItem>
|
||||||
))}
|
))}
|
||||||
</Contents>
|
</Contents>
|
||||||
</Container>
|
</Container>
|
||||||
@ -76,6 +78,7 @@ const Container = styled.aside<{ dy: number; height: number }>`
|
|||||||
padding-bottom: 72px;
|
padding-bottom: 72px;
|
||||||
|
|
||||||
transition: opacity 0.3s ease-in-out;
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
@ -104,9 +107,10 @@ const Contents = styled.div<{ height: number }>`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const Section = styled.section<{ active: boolean }>`
|
const TocItem = styled(Link)<{ active: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 8px 0 8px 12px;
|
padding: 8px 0 8px 12px;
|
||||||
|
text-decoration: none;
|
||||||
border-left: ${(p) =>
|
border-left: ${(p) =>
|
||||||
p.active
|
p.active
|
||||||
? '1px solid rgb(var(--lsd-border-primary))'
|
? '1px solid rgb(var(--lsd-border-primary))'
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { PostListLayout } from '@/types/ui.types'
|
|
||||||
|
|
||||||
export const uiConfigs = {
|
export const uiConfigs = {
|
||||||
navbarRenderedHeight: 45,
|
navbarRenderedHeight: 45,
|
||||||
postSectionMargin: 78,
|
postSectionMargin: 78,
|
||||||
maxContainerWidth: 1400,
|
maxContainerWidth: 1400,
|
||||||
|
articleRenderedMT: 45 * 2,
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
export type ArticleContainerContextType = {
|
export type ArticleContainerContextType = {
|
||||||
tocIndex: number
|
tocId: string | null
|
||||||
setTocIndex: React.Dispatch<React.SetStateAction<number>>
|
setTocId: React.Dispatch<React.SetStateAction<string | null>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ArticleContainerContext =
|
export const ArticleContainerContext =
|
||||||
|
@ -13,12 +13,10 @@ interface Props {
|
|||||||
|
|
||||||
const ArticleContainer = (props: Props) => {
|
const ArticleContainer = (props: Props) => {
|
||||||
const { data } = props
|
const { data } = props
|
||||||
const [tocIndex, setTocIndex] = useState(0)
|
const [tocId, setTocId] = useState<string | null>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ArticleContainerContext.Provider
|
<ArticleContainerContext.Provider value={{ tocId, setTocId }}>
|
||||||
value={{ tocIndex: tocIndex, setTocIndex: setTocIndex }}
|
|
||||||
>
|
|
||||||
<Grid
|
<Grid
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
@ -58,17 +58,27 @@ export default function App({ Component, pageProps }: AppLayoutProps) {
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--lpe-nav-rendered-height: ${uiConfigs.navbarRenderedHeight}px;
|
--lpe-nav-rendered-height: ${uiConfigs.navbarRenderedHeight}px;
|
||||||
|
--lpe-article-rendered-margin-top: ${uiConfigs.articleRenderedMT}px;
|
||||||
}
|
}
|
||||||
|
|
||||||
//.lazyload,
|
a,
|
||||||
//img.lazyloading {
|
a:visited,
|
||||||
// opacity: 0;
|
a:hover,
|
||||||
// transition: opacity 4000ms;
|
a:active,
|
||||||
//}
|
a:focus {
|
||||||
//
|
color: rgb(var(--lsd-text-primary));
|
||||||
//img.lazyloaded {
|
}
|
||||||
// opacity: 1;
|
|
||||||
//}
|
.anchor {
|
||||||
|
margin-top: calc(-1 * var(--lpe-article-rendered-margin-top));
|
||||||
|
padding-bottom: var(--lpe-article-rendered-margin-top);
|
||||||
|
margin-bottom: -16px;
|
||||||
|
display: block;
|
||||||
|
height: 0;
|
||||||
|
background: red;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
<ProgressBar />
|
<ProgressBar />
|
||||||
|
@ -309,7 +309,8 @@ class UnbodyService extends UnbodyClient {
|
|||||||
|
|
||||||
return this.request<UnbodyGraphQlResponseGoogleDoc>(query)
|
return this.request<UnbodyGraphQlResponseGoogleDoc>(query)
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
if (!data) return this.handleResponse([], 'No data')
|
if (!data || !data.Get || !data.Get.GoogleDoc)
|
||||||
|
return this.handleResponse([], 'No data')
|
||||||
return this.handleResponse(data.Get.GoogleDoc.map(enhanceGoogleDoc))
|
return this.handleResponse(data.Get.GoogleDoc.map(enhanceGoogleDoc))
|
||||||
})
|
})
|
||||||
.catch((e) => this.handleResponse([], e))
|
.catch((e) => this.handleResponse([], e))
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { RefObject, useEffect, useRef, useState } from 'react'
|
import { MutableRefObject, RefObject, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
export const useSticky = <T extends HTMLElement>(dy: number = 0) => {
|
export const useSticky = <T extends HTMLElement>(dy: number = 0) => {
|
||||||
const stickyRef = useRef<T>(null)
|
const stickyRef = useRef<T>(null)
|
||||||
@ -62,3 +62,41 @@ export const useIsScrolling = () => {
|
|||||||
}, [setIsScrolling])
|
}, [setIsScrolling])
|
||||||
return isScrolling
|
return isScrolling
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HeadingElementsRef = MutableRefObject<{
|
||||||
|
[key: string]: HTMLHeadingElement | null
|
||||||
|
}>
|
||||||
|
|
||||||
|
export function useIntersectionObserver(
|
||||||
|
setActiveId: (id: string) => void,
|
||||||
|
): HeadingElementsRef {
|
||||||
|
const headingElementsRef: HeadingElementsRef = useRef({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const callback = (headings: IntersectionObserverEntry[]) => {
|
||||||
|
headings.forEach((heading) => {
|
||||||
|
if (heading.isIntersecting && heading.target instanceof HTMLElement) {
|
||||||
|
const targetId = heading.target.getAttribute('id')
|
||||||
|
if (targetId) setActiveId(targetId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(callback, {
|
||||||
|
rootMargin: '0px 0px -80% 0px',
|
||||||
|
})
|
||||||
|
|
||||||
|
const current = headingElementsRef.current
|
||||||
|
Object.values(current).forEach((element) => {
|
||||||
|
if (element) observer.observe(element)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Object.values(current).forEach((element) => {
|
||||||
|
if (element) observer.unobserve(element)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [setActiveId])
|
||||||
|
|
||||||
|
return headingElementsRef
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user