feature: 1. add TOC;

This commit is contained in:
mitsuha 2022-08-24 19:47:58 +08:00
parent 13fe35ad61
commit 16a99c7507
4 changed files with 66 additions and 51 deletions

View File

@ -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<TocType[]>([]);
const [tocDataSource, setTocDataSource] = useState<TOCType[]>([]);
const [activeBlockId, setActiveBlockId] = useState('');
/* store page/block unmount-listener */
const listenerMapRef = useRef<ListenerMap>(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) => {

View File

@ -0,0 +1,6 @@
export enum BLOCK_TYPES {
GROUP = 'group',
HEADING1 = 'heading1',
HEADING2 = 'heading2',
HEADING3 = 'heading3',
}

View File

@ -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<string, () => 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 };

View File

@ -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<string, () => void>;
export interface TOCProps {
children: ReactNode;
editor?: BlockEditor;
}