Skip to content

Commit

Permalink
feat: note layout right side
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <tukon479@gmail.com>
  • Loading branch information
Innei committed Jun 11, 2023
1 parent 87ab8a2 commit c4f80a3
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 108 deletions.
11 changes: 7 additions & 4 deletions src/app/notes/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useParams } from 'next/navigation'
import { Toc } from '~/components/widgets/toc'
import { useNoteByNidQuery } from '~/hooks/data/use-note'
import { PageDataHolder } from '~/lib/page-holder'
import { ArticleElementContextProvider } from '~/providers/article/article-element-provider'
import { NoteLayoutRightSidePortal } from '~/providers/note/right-side-provider'
import { parseMarkdown } from '~/remark'

Expand All @@ -19,10 +20,12 @@ const PageImpl = () => {
<header>
<h1>{data?.data?.title}</h1>
</header>
{mardownResult.jsx}
<NoteLayoutRightSidePortal>
<Toc toc={mardownResult.toc} className="sticky top-20 mt-20" />
</NoteLayoutRightSidePortal>
<ArticleElementContextProvider>
{mardownResult.jsx}
<NoteLayoutRightSidePortal>
<Toc className="sticky top-20 mt-20" />
</NoteLayoutRightSidePortal>
</ArticleElementContextProvider>
</article>
)
}
Expand Down
5 changes: 2 additions & 3 deletions src/app/notes/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ export default async (props: PropsWithChildren) => {
<div className="relative md:col-start-1 lg:col-auto">
{props.children}
</div>
<div className="hidden lg:block">
<NoteLayoutRightSideProvider />
</div>

<NoteLayoutRightSideProvider className="relative hidden lg:block" />
</div>
)
}
44 changes: 36 additions & 8 deletions src/components/widgets/toc/Toc.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,41 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { TocItem as ITocItem } from '~/remark'
import type { ITocItem } from './TocItem'

import { RightToLeftTransitionView } from '~/components/ui/transition/RightToLeftTransitionView'
import { throttle } from '~/lib/_'
import { useArticleElement } from '~/providers/article/article-element-provider'
import { clsxm } from '~/utils/helper'

import { TocItem } from './TocItem'

export type TocProps = {
toc: ITocItem[]

useAsWeight?: boolean
}

export const Toc: Component<TocProps> = ({ toc, useAsWeight, className }) => {
export const Toc: Component<TocProps> = ({ useAsWeight, className }) => {
const containerRef = useRef<HTMLUListElement>(null)
const $article = useArticleElement()
const $headings = useMemo(() => {
if (!$article) {
return []
}
return $article.querySelectorAll('h1,h2,h3,h4,h5,h6')
}, [$article])
const toc: ITocItem[] = useMemo(() => {
return Array.from($headings).map((el, idx) => {
const depth = +el.tagName.slice(1)
const title = el.textContent || ''

const index = idx

return {
depth,
index: isNaN(index) ? -1 : index,
title,
url: `#${el.id}`,
}
})
}, [$headings])

const [index, setIndex] = useState(-1)
// useEffect(() => {
Expand Down Expand Up @@ -75,7 +96,7 @@ export const Toc: Component<TocProps> = ({ toc, useAsWeight, className }) => {
{toc?.map((heading) => {
return (
<MemoedItem
containerRef={useAsWeight ? undefined : containerRef}
// containerRef={useAsWeight ? undefined : containerRef}
heading={heading}
isActive={heading.index === index}
onClick={handleItemClick}
Expand All @@ -94,9 +115,15 @@ const MemoedItem = memo<{
heading: ITocItem
rootDepth: number
onClick: (i: number) => void
containerRef: any
// containerRef: any
}>((props) => {
const { heading, isActive, onClick, rootDepth, containerRef } = props
const {
heading,
isActive,
onClick,
rootDepth,
// containerRef
} = props

return (
<RightToLeftTransitionView
Expand All @@ -111,7 +138,8 @@ const MemoedItem = memo<{
className="leading-none"
>
<TocItem
containerRef={containerRef}
anchorId={heading.url}
// containerRef={containerRef}
index={heading.index}
onClick={onClick}
active={isActive}
Expand Down
55 changes: 23 additions & 32 deletions src/components/widgets/toc/TocItem.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,41 @@
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
import { tv } from 'tailwind-variants'
import type { FC, MouseEvent, RefObject } from 'react'
import type { FC, MouseEvent } from 'react'

import { useArticleElement } from '~/providers/article/article-element-provider'
import { clsxm } from '~/utils/helper'
import { springScrollToElement } from '~/utils/spring'

const styles = tv({
base: 'leading-normal mb-[1.5px] text-neutral-content inline-block relative max-w-full min-w-0 truncate text-left opacity-50 transition-all tabular-nums hover:opacity-80',
base: clsxm(
'leading-normal mb-[1.5px] text-neutral-content inline-block relative max-w-full min-w-0',
'truncate text-left opacity-50 transition-all tabular-nums hover:opacity-80 duration-500',
),
variants: {
status: {
active: 'ml-2 opacity-100',
},
},
})
export interface ITocItem {
depth: number
title: string
url: string
index: number
}

export const TocItem: FC<{
title: string
anchorId: string
depth: number
active: boolean
rootDepth: number
onClick: (i: number) => void
index: number
containerRef?: RefObject<HTMLDivElement>
// containerRef?: RefObject<HTMLDivElement>
}> = memo((props) => {
const { index, active, depth, title, rootDepth, onClick, containerRef } =
props
const { index, active, depth, title, rootDepth, onClick, anchorId } = props

const $ref = useRef<HTMLAnchorElement>(null)
useEffect(() => {
if (active) {
Expand All @@ -33,35 +44,12 @@ export const TocItem: FC<{
}
}, [active, title])

useEffect(() => {
if (!$ref.current || !active || !containerRef?.current) {
return
}
// NOTE animation group will wrap a element as a scroller container
const $scoller = containerRef.current.children?.item(0)

if (!$scoller) {
return
}
const itemHeight = $ref.current.offsetHeight
const currentScrollerTop = $scoller.scrollTop
const scollerContainerHeight = $scoller.clientHeight
const thisItemTop = index * itemHeight

if (
currentScrollerTop - thisItemTop >= 0 ||
(thisItemTop >= currentScrollerTop &&
thisItemTop >= scollerContainerHeight)
) {
$scoller.scrollTop = thisItemTop
}
}, [active, containerRef, index])

const renderDepth = useMemo(() => {
const result = depth - rootDepth

return result
}, [depth, rootDepth])
const $article = useArticleElement()
return (
<a
ref={$ref}
Expand All @@ -75,7 +63,7 @@ export const TocItem: FC<{
style={useMemo(
() => ({
paddingLeft:
depth >= rootDepth ? `${1.2 + renderDepth * 0.6}rem` : undefined,
depth >= rootDepth ? `${renderDepth * 0.6}rem` : undefined,
}),
[depth, renderDepth, rootDepth],
)}
Expand All @@ -84,12 +72,15 @@ export const TocItem: FC<{
(e: MouseEvent) => {
e.preventDefault()
onClick(index)
const $el = document.getElementById(title)
const $el = $article?.querySelector(
`${anchorId}`,
) as any as HTMLElement
console.log($el)
if ($el) {
springScrollToElement($el, -100)
}
},
[index, title, onClick],
[onClick, index, $article, anchorId],
)}
title={title}
>
Expand Down
38 changes: 38 additions & 0 deletions src/providers/article/article-element-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useEffect, useState } from 'react'
import { createContextState } from 'foxact/create-context-state'

import { clsxm } from '~/utils/helper'

const [
ArticleElementContextProviderInternal,
useArticleElement,
useSetArticleElement,
] = createContextState<HTMLDivElement | null>(null)

const ArticleElementContextProvider: Component = ({ children, className }) => {
return (
<ArticleElementContextProviderInternal>
<Content className={className}>{children}</Content>
</ArticleElementContextProviderInternal>
)
}

const Content: Component = ({ children, className }) => {
const [contentRef, setContentRef] = useState<HTMLDivElement | null>(null)
const setter = useSetArticleElement()
useEffect(() => {
setter(contentRef)
console.log(contentRef)
}, [contentRef, setter])
return (
<div className={clsxm('relative', className)} ref={setContentRef}>
{children}
</div>
)
}

export {
ArticleElementContextProvider,
useSetArticleElement,
useArticleElement,
}
34 changes: 18 additions & 16 deletions src/providers/note/right-side-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
'use client'

import React, { useEffect } from 'react'
import { createPortal } from 'react-dom'
import { atom, useAtomValue, useSetAtom } from 'jotai'
import type { FC } from 'react'

const rightSideAtom = atom<FC | JSX.Element | null>(null)
import { useIsClient } from '~/hooks/common/use-is-client'

const useSetNoteLayoutRightSideElement = () => useSetAtom(rightSideAtom)
const rightSideElementAtom = atom<null | HTMLDivElement>(null)
export const NoteLayoutRightSideProvider: Component = ({ className }) => {
const setElement = useSetAtom(rightSideElementAtom)

export const NoteLayoutRightSideProvider = () => {
const ReactNodeOrComponent = useAtomValue(rightSideAtom)

if (!ReactNodeOrComponent) return null
useEffect(() => {
return () => {
// GC
setElement(null)
}
}, [])

if (React.isValidElement(ReactNodeOrComponent)) return ReactNodeOrComponent
else if (typeof ReactNodeOrComponent === 'function')
return <ReactNodeOrComponent />
else return null
return <div ref={setElement} className={className} />
}

export const NoteLayoutRightSidePortal: Component = ({ children }) => {
const setter = useSetNoteLayoutRightSideElement()
const rightSideElement = useAtomValue(rightSideElementAtom)

useEffect(() => {
setter(<>{children}</>)
}, [children])
const isClient = useIsClient()
if (!isClient) return null

if (!rightSideElement) return null

return null
return createPortal(children, rightSideElement)
}
Loading

1 comment on commit c4f80a3

@vercel
Copy link

@vercel vercel bot commented on c4f80a3 Jun 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

springtide – ./

springtide-git-main-innei.vercel.app
springtide.vercel.app
springtide-innei.vercel.app

Please sign in to comment.