From c4f80a3f388f8fc18a133766b01a350d3210ff3e Mon Sep 17 00:00:00 2001 From: Innei Date: Sun, 11 Jun 2023 21:41:17 +0800 Subject: [PATCH] feat: note layout right side Signed-off-by: Innei --- src/app/notes/[id]/page.tsx | 11 ++-- src/app/notes/layout.tsx | 5 +- src/components/widgets/toc/Toc.tsx | 44 ++++++++++++--- src/components/widgets/toc/TocItem.tsx | 55 ++++++++----------- .../article/article-element-provider.tsx | 38 +++++++++++++ src/providers/note/right-side-provider.tsx | 34 ++++++------ src/remark/index.ts | 51 +++-------------- src/utils/spring.ts | 6 +- 8 files changed, 136 insertions(+), 108 deletions(-) create mode 100644 src/providers/article/article-element-provider.tsx diff --git a/src/app/notes/[id]/page.tsx b/src/app/notes/[id]/page.tsx index e337543bea..af9e594912 100644 --- a/src/app/notes/[id]/page.tsx +++ b/src/app/notes/[id]/page.tsx @@ -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' @@ -19,10 +20,12 @@ const PageImpl = () => {

{data?.data?.title}

- {mardownResult.jsx} - - - + + {mardownResult.jsx} + + + + ) } diff --git a/src/app/notes/layout.tsx b/src/app/notes/layout.tsx index 8eeea3eaf1..d9f956c5ef 100644 --- a/src/app/notes/layout.tsx +++ b/src/app/notes/layout.tsx @@ -18,9 +18,8 @@ export default async (props: PropsWithChildren) => {
{props.children}
-
- -
+ + ) } diff --git a/src/components/widgets/toc/Toc.tsx b/src/components/widgets/toc/Toc.tsx index e44044a6b4..d43df3a52a 100644 --- a/src/components/widgets/toc/Toc.tsx +++ b/src/components/widgets/toc/Toc.tsx @@ -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 = ({ toc, useAsWeight, className }) => { +export const Toc: Component = ({ useAsWeight, className }) => { const containerRef = useRef(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(() => { @@ -75,7 +96,7 @@ export const Toc: Component = ({ toc, useAsWeight, className }) => { {toc?.map((heading) => { return ( void - containerRef: any + // containerRef: any }>((props) => { - const { heading, isActive, onClick, rootDepth, containerRef } = props + const { + heading, + isActive, + onClick, + rootDepth, + // containerRef + } = props return ( void index: number - containerRef?: RefObject + // containerRef?: RefObject }> = memo((props) => { - const { index, active, depth, title, rootDepth, onClick, containerRef } = - props + const { index, active, depth, title, rootDepth, onClick, anchorId } = props + const $ref = useRef(null) useEffect(() => { if (active) { @@ -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 ( ({ paddingLeft: - depth >= rootDepth ? `${1.2 + renderDepth * 0.6}rem` : undefined, + depth >= rootDepth ? `${renderDepth * 0.6}rem` : undefined, }), [depth, renderDepth, rootDepth], )} @@ -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} > diff --git a/src/providers/article/article-element-provider.tsx b/src/providers/article/article-element-provider.tsx new file mode 100644 index 0000000000..7cb1c0dbec --- /dev/null +++ b/src/providers/article/article-element-provider.tsx @@ -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(null) + +const ArticleElementContextProvider: Component = ({ children, className }) => { + return ( + + {children} + + ) +} + +const Content: Component = ({ children, className }) => { + const [contentRef, setContentRef] = useState(null) + const setter = useSetArticleElement() + useEffect(() => { + setter(contentRef) + console.log(contentRef) + }, [contentRef, setter]) + return ( +
+ {children} +
+ ) +} + +export { + ArticleElementContextProvider, + useSetArticleElement, + useArticleElement, +} diff --git a/src/providers/note/right-side-provider.tsx b/src/providers/note/right-side-provider.tsx index fe21b7af56..09c43b312a 100644 --- a/src/providers/note/right-side-provider.tsx +++ b/src/providers/note/right-side-provider.tsx @@ -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(null) +import { useIsClient } from '~/hooks/common/use-is-client' -const useSetNoteLayoutRightSideElement = () => useSetAtom(rightSideAtom) +const rightSideElementAtom = atom(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 - else return null + return
} 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) } diff --git a/src/remark/index.ts b/src/remark/index.ts index 4c169138b0..04c6ef99db 100644 --- a/src/remark/index.ts +++ b/src/remark/index.ts @@ -1,54 +1,35 @@ import { createElement, Fragment } from 'react' import rehypeReact from 'rehype-react' -import { toc } from 'mdast-util-toc' import rehypeAutolinkHeadings from 'rehype-autolink-headings' import rehypeSlug from 'rehype-slug' import remarkGfm from 'remark-gfm' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' import { unified } from 'unified' -import type { Result as TocResult } from 'mdast-util-toc' import type { ReactNode } from 'react' import { rehypeWrapCode } from './rehype-wrap-code' interface ParserResult { jsx: ReactNode - toc: TocItem[] -} - -export interface TocItem { - depth: number - title: string - url: string - index: number } export const parseMarkdown = (markdownText: string): ParserResult => { const result: ParserResult = { jsx: null, - toc: [] as TocItem[], } const pipeline = unified() .use(remarkParse) - .use(() => (tree) => { - const tocResult = toc(tree, { tight: true, ordered: true }) - // TODO - const titles = parseTocTree(tocResult.map) - - result.toc = titles.map((title, index) => { - return { - title, - url: `#${title}`, - // TODO - depth: 1, - index, - } - }) - }) .use(remarkGfm, { singleTilde: false, }) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeSlug) + // .use(() => (tree) => { + // visit(tree, 'element', (node) => { + // console.log(node) + // }) + // }) .use(rehypeAutolinkHeadings, { properties: { className: ['springtide-anchor'], @@ -68,8 +49,7 @@ export const parseMarkdown = (markdownText: string): ParserResult => { ] }, }) - .use(rehypeSlug) - .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeWrapCode) .use(rehypeReact, { createElement, @@ -81,18 +61,3 @@ export const parseMarkdown = (markdownText: string): ParserResult => { return result } - -function parseTocTree(items: TocResult['map']) { - return ( - items?.children?.reduce((acc: string[], item) => { - item.children.forEach((child) => { - if (child.type === 'paragraph' && (child.children[0] as any).url) { - acc.push((child.children[0] as any).url.slice(1)) - } else if (child.type === 'list') { - acc.push(...parseTocTree(child)) - } - }) - return acc - }, []) || [] - ) -} diff --git a/src/utils/spring.ts b/src/utils/spring.ts index 0882fc5136..ac6de406c7 100644 --- a/src/utils/spring.ts +++ b/src/utils/spring.ts @@ -6,12 +6,14 @@ import { microdampingPreset } from '~/constants/spring' export const springScrollTo = (y: number) => { const scrollTop = - document.documentElement.scrollTop || document.body.scrollTop + // FIXME latest version framer will ignore keyframes value `0` + (document.documentElement.scrollTop || document.body.scrollTop) + 1 const animation = animateValue({ keyframes: [scrollTop, y], autoplay: true, ...microdampingPreset, onUpdate(latest) { + console.log(latest, 'latest') if (latest <= 0) { animation.stop() } @@ -23,9 +25,9 @@ export const springScrollToTop = () => { springScrollTo(0) } -// TODO check it export const springScrollToElement = (element: HTMLElement, delta = 40) => { const y = calculateElementTop(element) + const to = y + delta springScrollTo(to) }