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 { styled } from '@toeverything/components/ui';
import type { ReactNode } from 'react';
import { import {
createContext, createContext,
useCallback, useCallback,
useContext, useContext,
useEffect, useEffect,
useRef,
useState, useState,
} from 'react'; } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { BLOCK_TYPES } from './toc-enum';
import { import {
BLOCK_TYPES,
destroyEventList, destroyEventList,
getContentByAsyncBlocks, getContentByAsyncBlocks,
getPageTOC, getPageTOC,
listenerMap, } from './toc-util';
type TocType, import type { ListenerMap, TOCProps, TOCType } from './types';
} from '../../utils/toc';
const StyledTOCItem = styled('a')<{ type?: string; isActive?: boolean }>( const StyledTOCItem = styled('a')<{ type?: string; isActive?: boolean }>(
({ type, isActive }) => { ({ type, isActive }) => {
@@ -81,11 +79,6 @@ const StyledItem = styled('div')(props => {
const TOCContext = createContext(null); const TOCContext = createContext(null);
interface Props {
children: ReactNode;
editor: BlockEditor;
}
const TOCItem = props => { const TOCItem = props => {
const { activeBlockId, onClick } = useContext(TOCContext); const { activeBlockId, onClick } = useContext(TOCContext);
const { id, type, text } = props; 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 { editor } = props;
const { page_id } = useParams(); const { page_id } = useParams();
const [tocDataSource, setTocDataSource] = useState<TocType[]>([]); const [tocDataSource, setTocDataSource] = useState<TOCType[]>([]);
const [activeBlockId, setActiveBlockId] = useState(''); const [activeBlockId, setActiveBlockId] = useState('');
/* store page/block unmount-listener */
const listenerMapRef = useRef<ListenerMap>(new Map());
const updateTocDataSource = useCallback(async () => { const updateTocDataSource = useCallback(async () => {
if (!editor) {
return null;
}
const listenerMap = listenerMapRef.current;
/* page listener: trigger update-notice when add new group */ /* page listener: trigger update-notice when add new group */
const pageAsyncBlock = (await editor.getBlockByIds([page_id]))?.[0]; const pageAsyncBlock = (await editor.getBlockByIds([page_id]))?.[0];
if (!listenerMap.has(pageAsyncBlock.id)) { if (!listenerMap.has(pageAsyncBlock.id)) {
@@ -141,15 +143,13 @@ export const TOC = (props: Props) => {
const asyncBlocks = (await editor.getBlockByIds(children)) || []; const asyncBlocks = (await editor.getBlockByIds(children)) || [];
const { tocContents } = await getContentByAsyncBlocks( const { tocContents } = await getContentByAsyncBlocks(
asyncBlocks, asyncBlocks,
updateTocDataSource updateTocDataSource,
listenerMap
); );
/* toc: flat content */ /* toc: flat content */
const tocDataSource = getPageTOC(asyncBlocks, tocContents); const tocDataSource = getPageTOC(asyncBlocks, tocContents);
setTocDataSource(tocDataSource); setTocDataSource(tocDataSource);
/* remove listener when unmount component */
return destroyEventList;
}, [editor, page_id]); }, [editor, page_id]);
/* init toc and add page/block update-listener & unmount-listener */ /* init toc and add page/block update-listener & unmount-listener */
@@ -157,6 +157,9 @@ export const TOC = (props: Props) => {
(async () => { (async () => {
await updateTocDataSource(); await updateTocDataSource();
})(); })();
/* remove listener when unmount component */
return () => destroyEventList(listenerMapRef.current);
}, [updateTocDataSource]); }, [updateTocDataSource]);
const onClick = async (blockId?: string) => { 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'; import { AsyncBlock } from '@toeverything/components/editor-core';
import { BLOCK_TYPES } from './toc-enum';
export type TocType = { import type { ListenerMap, TOCType } from './types';
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>();
/* 😞😞sorry, I don't know how to define unlimited dimensions array */ /* 😞😞sorry, I don't know how to define unlimited dimensions array */
const getContentByAsyncBlocks = async ( const getContentByAsyncBlocks = async (
asyncBlocks: AsyncBlock[] = [], asyncBlocks: AsyncBlock[] = [],
callback: () => void callback: () => void,
listenerMap: ListenerMap
): Promise<{ ): Promise<{
tocContents: any[]; tocContents: any[];
}> => { }> => {
@@ -27,24 +14,29 @@ const getContentByAsyncBlocks = async (
/* maybe should recast it to tail recursion */ /* maybe should recast it to tail recursion */
return await Promise.all( return await Promise.all(
asyncBlocks.map(async (asyncBlock: AsyncBlock) => { asyncBlocks.map(async (asyncBlock: AsyncBlock) => {
const asyncBlocks = await asyncBlock.children(); const asyncBlocks = await asyncBlock?.children();
if (asyncBlocks?.length) { if (asyncBlocks?.length) {
return collect(asyncBlocks); return collect(asyncBlocks);
} }
/* add only once event listener for every block */ /* add only once event listener for every block */
if (!listenerMap.has(asyncBlock.id)) { if (!listenerMap.has(asyncBlock?.id)) {
/* get update notice */ /* get update notice */
const destroyHandler = asyncBlock.onUpdate(callback); const destroyHandler = asyncBlock?.onUpdate(callback);
/* collect destroy handlers */ /* collect destroy handlers */
listenerMap.set(asyncBlock.id, destroyHandler); listenerMap.set(asyncBlock?.id, destroyHandler);
} }
const { id, type } = asyncBlock; const { id, type } = asyncBlock;
if (Object.values(BLOCK_TYPES).includes(type)) {
const properties = await asyncBlock.getProperties(); switch (type) {
case BLOCK_TYPES.GROUP:
case BLOCK_TYPES.HEADING1:
case BLOCK_TYPES.HEADING2:
case BLOCK_TYPES.HEADING3: {
const properties = await asyncBlock?.getProperties();
return { return {
id, id,
@@ -52,8 +44,9 @@ const getContentByAsyncBlocks = async (
text: properties?.text?.value?.[0]?.text || '', text: properties?.text?.value?.[0]?.text || '',
}; };
} }
default:
return null; return null;
}
}) })
); );
}; };
@@ -68,7 +61,7 @@ const getContentByAsyncBlocks = async (
* @param asyncBlocks * @param asyncBlocks
* @param tocContents * @param tocContents
*/ */
const getPageTOC = (asyncBlocks: AsyncBlock[], tocContents): TocType[] => { const getPageTOC = (asyncBlocks: AsyncBlock[], tocContents): TOCType[] => {
return tocContents return tocContents
.reduce((tocGroupContent, tocContent, index) => { .reduce((tocGroupContent, tocContent, index) => {
const { id, type } = asyncBlocks[index]; const { id, type } = asyncBlocks[index];
@@ -91,15 +84,13 @@ const getPageTOC = (asyncBlocks: AsyncBlock[], tocContents): TocType[] => {
}; };
/* destroy page/block update-listener */ /* destroy page/block update-listener */
const destroyEventList = (): boolean => { const destroyEventList = (listenerMap: ListenerMap) => {
const eventListeners = listenerMap.values(); const eventListeners = listenerMap.values();
listenerMap.clear(); listenerMap.clear();
for (const eventListener of eventListeners) { for (const eventListener of eventListeners) {
eventListener?.(); eventListener?.();
} }
return true;
}; };
export { getPageTOC, getContentByAsyncBlocks, destroyEventList }; 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;
}