diff --git a/apps/ligo-virgo/src/pages/workspace/docs/components/toc/TOC.tsx b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/TOC.tsx index a4c4fba27e..9ba54dfabf 100644 --- a/apps/ligo-virgo/src/pages/workspace/docs/components/toc/TOC.tsx +++ b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/TOC.tsx @@ -1,22 +1,20 @@ -import { BlockEditor } from '@toeverything/components/editor-core'; import { styled } from '@toeverything/components/ui'; -import type { ReactNode } from 'react'; import { createContext, useCallback, useContext, useEffect, + useRef, useState, } from 'react'; import { useParams } from 'react-router'; +import { BLOCK_TYPES } from './toc-enum'; import { - BLOCK_TYPES, destroyEventList, getContentByAsyncBlocks, getPageTOC, - listenerMap, - type TocType, -} from '../../utils/toc'; +} from './toc-util'; +import type { ListenerMap, TOCProps, TOCType } from './types'; const StyledTOCItem = styled('a')<{ type?: string; isActive?: boolean }>( ({ type, isActive }) => { @@ -81,11 +79,6 @@ const StyledItem = styled('div')(props => { const TOCContext = createContext(null); -interface Props { - children: ReactNode; - editor: BlockEditor; -} - const TOCItem = props => { const { activeBlockId, onClick } = useContext(TOCContext); const { id, type, text } = props; @@ -119,13 +112,22 @@ const renderTOCContent = tocDataSource => { ); }; -export const TOC = (props: Props) => { +export const TOC = (props: TOCProps) => { const { editor } = props; const { page_id } = useParams(); - const [tocDataSource, setTocDataSource] = useState([]); + const [tocDataSource, setTocDataSource] = useState([]); const [activeBlockId, setActiveBlockId] = useState(''); + /* store page/block unmount-listener */ + const listenerMapRef = useRef(new Map()); + const updateTocDataSource = useCallback(async () => { + if (!editor) { + return null; + } + + const listenerMap = listenerMapRef.current; + /* page listener: trigger update-notice when add new group */ const pageAsyncBlock = (await editor.getBlockByIds([page_id]))?.[0]; if (!listenerMap.has(pageAsyncBlock.id)) { @@ -141,15 +143,13 @@ export const TOC = (props: Props) => { const asyncBlocks = (await editor.getBlockByIds(children)) || []; const { tocContents } = await getContentByAsyncBlocks( asyncBlocks, - updateTocDataSource + updateTocDataSource, + listenerMap ); /* toc: flat content */ const tocDataSource = getPageTOC(asyncBlocks, tocContents); setTocDataSource(tocDataSource); - - /* remove listener when unmount component */ - return destroyEventList; }, [editor, page_id]); /* init toc and add page/block update-listener & unmount-listener */ @@ -157,6 +157,9 @@ export const TOC = (props: Props) => { (async () => { await updateTocDataSource(); })(); + + /* remove listener when unmount component */ + return () => destroyEventList(listenerMapRef.current); }, [updateTocDataSource]); const onClick = async (blockId?: string) => { diff --git a/apps/ligo-virgo/src/pages/workspace/docs/components/toc/toc-enum.ts b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/toc-enum.ts new file mode 100644 index 0000000000..5c9ad0c59e --- /dev/null +++ b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/toc-enum.ts @@ -0,0 +1,6 @@ +export enum BLOCK_TYPES { + GROUP = 'group', + HEADING1 = 'heading1', + HEADING2 = 'heading2', + HEADING3 = 'heading3', +} diff --git a/apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/toc-util.ts similarity index 62% rename from apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts rename to apps/ligo-virgo/src/pages/workspace/docs/components/toc/toc-util.ts index 9b550e9bbe..aa1154a944 100644 --- a/apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts +++ b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/toc-util.ts @@ -1,25 +1,12 @@ import { AsyncBlock } from '@toeverything/components/editor-core'; - -export type TocType = { - id: string; - type: string; - text: string; -}; - -export const BLOCK_TYPES = { - GROUP: 'group', - HEADING1: 'heading1', - HEADING2: 'heading2', - HEADING3: 'heading3', -}; - -/* store page/block unmount-listener */ -export const listenerMap = new Map void>(); +import { BLOCK_TYPES } from './toc-enum'; +import type { ListenerMap, TOCType } from './types'; /* ๐Ÿ˜ž๐Ÿ˜žsorry, I don't know how to define unlimited dimensions array */ const getContentByAsyncBlocks = async ( asyncBlocks: AsyncBlock[] = [], - callback: () => void + callback: () => void, + listenerMap: ListenerMap ): Promise<{ tocContents: any[]; }> => { @@ -27,33 +14,39 @@ const getContentByAsyncBlocks = async ( /* maybe should recast it to tail recursion */ return await Promise.all( asyncBlocks.map(async (asyncBlock: AsyncBlock) => { - const asyncBlocks = await asyncBlock.children(); + const asyncBlocks = await asyncBlock?.children(); if (asyncBlocks?.length) { return collect(asyncBlocks); } /* add only once event listener for every block */ - if (!listenerMap.has(asyncBlock.id)) { + if (!listenerMap.has(asyncBlock?.id)) { /* get update notice */ - const destroyHandler = asyncBlock.onUpdate(callback); + const destroyHandler = asyncBlock?.onUpdate(callback); /* collect destroy handlers */ - listenerMap.set(asyncBlock.id, destroyHandler); + listenerMap.set(asyncBlock?.id, destroyHandler); } const { id, type } = asyncBlock; - if (Object.values(BLOCK_TYPES).includes(type)) { - const properties = await asyncBlock.getProperties(); - return { - id, - type, - text: properties?.text?.value?.[0]?.text || '', - }; + switch (type) { + case BLOCK_TYPES.GROUP: + case BLOCK_TYPES.HEADING1: + case BLOCK_TYPES.HEADING2: + case BLOCK_TYPES.HEADING3: { + const properties = await asyncBlock?.getProperties(); + + return { + id, + type, + text: properties?.text?.value?.[0]?.text || '', + }; + } + default: + return null; } - - return null; }) ); }; @@ -68,7 +61,7 @@ const getContentByAsyncBlocks = async ( * @param asyncBlocks * @param tocContents */ -const getPageTOC = (asyncBlocks: AsyncBlock[], tocContents): TocType[] => { +const getPageTOC = (asyncBlocks: AsyncBlock[], tocContents): TOCType[] => { return tocContents .reduce((tocGroupContent, tocContent, index) => { const { id, type } = asyncBlocks[index]; @@ -91,15 +84,13 @@ const getPageTOC = (asyncBlocks: AsyncBlock[], tocContents): TocType[] => { }; /* destroy page/block update-listener */ -const destroyEventList = (): boolean => { +const destroyEventList = (listenerMap: ListenerMap) => { const eventListeners = listenerMap.values(); listenerMap.clear(); for (const eventListener of eventListeners) { eventListener?.(); } - - return true; }; export { getPageTOC, getContentByAsyncBlocks, destroyEventList }; diff --git a/apps/ligo-virgo/src/pages/workspace/docs/components/toc/types.ts b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/types.ts new file mode 100644 index 0000000000..6c5696884b --- /dev/null +++ b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/types.ts @@ -0,0 +1,15 @@ +import type { BlockEditor } from '@toeverything/components/editor-core'; +import type { ReactNode } from 'react'; + +export type TOCType = { + id: string; + type: string; + text: string; +}; + +export type ListenerMap = Map void>; + +export interface TOCProps { + children: ReactNode; + editor?: BlockEditor; +}