feature: 1. add TOC;
This commit is contained in:
@@ -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) => {
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export enum BLOCK_TYPES {
|
||||||
|
GROUP = 'group',
|
||||||
|
HEADING1 = 'heading1',
|
||||||
|
HEADING2 = 'heading2',
|
||||||
|
HEADING3 = 'heading3',
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user