From ab070daa7ee511fb78b48ef26e0af6e7c93d4b29 Mon Sep 17 00:00:00 2001 From: mitsuha Date: Tue, 23 Aug 2022 15:37:44 +0800 Subject: [PATCH 1/6] feat: 1. add toc; --- README.md | 2 +- .../src/pages/workspace/docs/Page.tsx | 67 ++++--- .../workspace/docs/components/tabs/Tabs.tsx | 36 ++-- .../workspace/docs/components/toc/TOC.tsx | 168 ++++++++++++++++++ .../workspace/docs/components/toc/index.ts | 1 + .../docs/utils/getPageContentById.ts | 64 +++++++ .../editor-core/src/editor/editor.ts | 13 ++ .../db-service/src/services/index.ts | 9 + .../db-service/src/services/workspace/toc.ts | 28 +++ 9 files changed, 350 insertions(+), 38 deletions(-) create mode 100644 apps/ligo-virgo/src/pages/workspace/docs/components/toc/TOC.tsx create mode 100644 apps/ligo-virgo/src/pages/workspace/docs/components/toc/index.ts create mode 100644 apps/ligo-virgo/src/pages/workspace/docs/utils/getPageContentById.ts create mode 100644 libs/datasource/db-service/src/services/workspace/toc.ts diff --git a/README.md b/README.md index 718d32c94d..19e0db1307 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ Our latest news can be found on [Twitter](https://twitter.com/AffineOfficial), [ # Contact Us -You may contact us by emailing to: contact@toeverything.info +You may contact us by emailing to: contact@toeverything.info # The Philosophy of AFFiNE diff --git a/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx b/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx index a2440a1757..6d9d2fac90 100644 --- a/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx +++ b/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx @@ -22,16 +22,29 @@ import { type BlockEditor } from '@toeverything/components/editor-core'; import { useFlag } from '@toeverything/datasource/feature-flags'; import { CollapsiblePageTree } from './collapsible-page-tree'; import { Tabs } from './components/tabs'; +import { TabMap, TAB_TITLE } from './components/tabs/Tabs'; +import { TOC } from './components/toc'; import { WorkspaceName } from './workspace-name'; + type PageProps = { workspace: string; }; export function Page(props: PageProps) { + const [activeTab, setActiveTab] = useState( + TabMap.get(TAB_TITLE.PAGES).value + ); const { page_id } = useParams(); const { showSpaceSidebar, fixedDisplay, setSpaceSidebarVisible } = useShowSpaceSidebar(); const dailyNotesFlag = useFlag('BooleanDailyNotes', false); + const editorRef = useRef(null); + + const onTabChange = v => setActiveTab(v); + + const getEditor = editor => { + editorRef.current = editor; + }; return ( @@ -50,35 +63,45 @@ export function Page(props: PageProps) { > - + -
- {dailyNotesFlag && ( + {activeTab === TabMap.get(TAB_TITLE.PAGES).value && ( +
+ {dailyNotesFlag && ( +
+ + + +
+ )}
- - + +
- )} -
- - - +
+ + {page_id ? : null} + +
-
- - {page_id ? : null} - -
-
+ )} + + {activeTab === TabMap.get(TAB_TITLE.TOC).value && ( + TOC + )} - + ); } @@ -86,9 +109,11 @@ export function Page(props: PageProps) { const EditorContainer = ({ pageId, workspace, + getEditor, }: { pageId: string; workspace: string; + getEditor: (editor: BlockEditor) => void; }) => { const [lockScroll, setLockScroll] = useState(false); const [scrollContainer, setScrollContainer] = useState(); @@ -105,6 +130,8 @@ const EditorContainer = ({ const obv = new ResizeObserver(e => { setPageClientWidth(e[0].contentRect.width); }); + + getEditor(editorRef.current); obv.observe(scrollContainer); return () => obv.disconnect(); } diff --git a/apps/ligo-virgo/src/pages/workspace/docs/components/tabs/Tabs.tsx b/apps/ligo-virgo/src/pages/workspace/docs/components/tabs/Tabs.tsx index 74a8dcc5ae..aea0c88374 100644 --- a/apps/ligo-virgo/src/pages/workspace/docs/components/tabs/Tabs.tsx +++ b/apps/ligo-virgo/src/pages/workspace/docs/components/tabs/Tabs.tsx @@ -1,6 +1,5 @@ import { styled } from '@toeverything/components/ui'; import type { ValueOf } from '@toeverything/utils'; -import { useState } from 'react'; const StyledTabs = styled('div')(({ theme }) => { return { @@ -56,32 +55,35 @@ const StyledTabTitle = styled('div')<{ } `; -const TAB_TITLE = { - PAGES: 'pages', - GALLERY: 'gallery', - TOC: 'toc', +export const TAB_TITLE = { + PAGES: 'PAGES', + GALLERY: 'GALLERY', + TOC: 'TOC', } as const; -const TabMap = new Map([ - ['PAGES', { value: 'pages' }], - ['GALLERY', { value: 'gallery', disabled: true }], - ['TOC', { value: 'toc' }], +export const TabMap = new Map< + TabValue, + { value: TabValue; disabled?: boolean } +>([ + [TAB_TITLE.PAGES, { value: TAB_TITLE.PAGES }], + [TAB_TITLE.GALLERY, { value: TAB_TITLE.GALLERY, disabled: true }], + [TAB_TITLE.TOC, { value: TAB_TITLE.TOC }], ]); -type TabKey = keyof typeof TAB_TITLE; type TabValue = ValueOf; -const Tabs = () => { - const [activeValue, setActiveTab] = useState(TAB_TITLE.PAGES); +interface Props { + activeTab: TabValue; + onTabChange: (v: TabValue) => void; +} - const onClick = (v: TabValue) => { - setActiveTab(v); - }; +const Tabs = (props: Props) => { + const { activeTab, onTabChange } = props; return ( {[...TabMap.entries()].map(([k, { value, disabled = false }]) => { - const isActive = activeValue === value; + const isActive = activeTab === value; return ( { className={isActive ? 'active' : ''} isActive={isActive} isDisabled={disabled} - onClick={() => onClick(value)} + onClick={() => onTabChange(value)} > {k} 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 new file mode 100644 index 0000000000..b2c0aa693e --- /dev/null +++ b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/TOC.tsx @@ -0,0 +1,168 @@ +import { BlockEditor } from '@toeverything/components/editor-core'; +import { styled } from '@toeverything/components/ui'; +import { services } from '@toeverything/datasource/db-service'; +import type { ReactNode } from 'react'; +import { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; +import { useParams } from 'react-router'; +import { + BLOCK_TYPES, + getPageContentById, +} from '../../utils/getPageContentById'; + +const StyledTOC = styled('div')(() => { + return {}; +}); + +const StyledTOCItem = styled('a')<{ type?: string; isActive?: boolean }>( + ({ type, isActive }) => { + const common = { + height: '32px', + display: 'flex', + alignItems: 'center', + cursor: 'pointer', + color: isActive ? '#3E6FDB' : '#4C6275', + }; + + if (type === BLOCK_TYPES.HEADING1) { + return { + ...common, + padding: '0 12px', + fontWeight: '600', + fontSize: '16px', + }; + } + + if (type === BLOCK_TYPES.HEADING2) { + return { + ...common, + padding: '0 32px', + fontSize: '14px', + }; + } + + if (type === BLOCK_TYPES.HEADING3) { + return { + ...common, + padding: '0 52px', + fontSize: '12px', + }; + } + + if (type === BLOCK_TYPES.GROUP) { + return { + ...common, + margin: '6px 0px', + height: '46px', + padding: '6px 12px', + fontWeight: '600', + fontSize: '16px', + borderTop: '0.5px solid #E0E6EB', + borderBottom: '0.5px solid #E0E6EB', + color: isActive ? '#3E6FDB' : '#98ACBD', + }; + } + + return {}; + } +); + +const StyledItem = styled('div')(props => { + return { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }; +}); + +const TOCContext = createContext(null); + +interface Props { + children: ReactNode; + editor: BlockEditor; +} + +const TOCItem = props => { + const { activeBlockId, onClick } = useContext(TOCContext); + const { id, type, text } = props; + const isActive = id === activeBlockId; + + return ( + onClick(id)} + > + {text} + + ); +}; + +const renderTOCContent = tocDataSource => { + return ( + <> + {tocDataSource.map(tocItem => { + if (tocItem?.length) { + return renderTOCContent(tocItem); + } + + const { id, type, text } = tocItem; + + return ; + })} + + ); +}; + +export const TOC = (props: Props) => { + const { editor } = props; + const { workspace_id, page_id } = useParams(); + const [tocDataSource, setTocDataSource] = useState([]); + const [activeBlockId, setActiveBlockId] = useState('blockId'); + + const updateTocDataSource = useCallback(async () => { + const tocDataSource = await getPageContentById(editor, page_id); + setTocDataSource(tocDataSource); + }, [editor, page_id]); + + useEffect(() => { + (async () => await updateTocDataSource())(); + }, [updateTocDataSource]); + + useEffect(() => { + let unobserve: () => void; + const observe = async () => { + unobserve = await services.api.tocService.observe( + { workspace: workspace_id, pageId: page_id }, + updateTocDataSource + ); + }; + void observe(); + + return () => { + unobserve?.(); + }; + }, [updateTocDataSource, workspace_id]); + + const onClick = async (blockId?: string) => { + if (blockId === activeBlockId) { + return; + } + + console.log(blockId); + setActiveBlockId(blockId); + await editor.scrollManager.scrollIntoViewByBlockId(blockId); + }; + + return ( + + {renderTOCContent(tocDataSource)} + + ); +}; diff --git a/apps/ligo-virgo/src/pages/workspace/docs/components/toc/index.ts b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/index.ts new file mode 100644 index 0000000000..e603af8242 --- /dev/null +++ b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/index.ts @@ -0,0 +1 @@ +export { TOC } from './TOC'; diff --git a/apps/ligo-virgo/src/pages/workspace/docs/utils/getPageContentById.ts b/apps/ligo-virgo/src/pages/workspace/docs/utils/getPageContentById.ts new file mode 100644 index 0000000000..b1d16cb9d6 --- /dev/null +++ b/apps/ligo-virgo/src/pages/workspace/docs/utils/getPageContentById.ts @@ -0,0 +1,64 @@ +import type { BlockEditor } from '@toeverything/components/editor-core'; + +export const BLOCK_TYPES = { + GROUP: 'group', + HEADING1: 'heading1', + HEADING2: 'heading2', + HEADING3: 'heading3', +}; + +const getContentByAsyncBlocks = async (asyncBlocks = []) => { + /* maybe should recast it to tail recursion */ + return await Promise.all( + asyncBlocks.map(async asyncBlock => { + const asyncBlocks = await asyncBlock.children(); + + if (asyncBlocks?.length) { + return getContentByAsyncBlocks(asyncBlocks); + } + + const { id, type } = asyncBlock; + + if (Object.values(BLOCK_TYPES).includes(type)) { + const properties = await asyncBlock.getProperties(); + + return { + id: id, + type, + text: properties?.text?.value?.[0]?.text || '', + }; + } + + return null; + }) + ); +}; + +const getPageContentById = async (editor: BlockEditor, pageId: string) => { + const { children = [] } = (await editor.queryByPageId(pageId))?.[0] || {}; + const asyncBlocks = (await editor.getBlockByIds(children)) || []; + + const tocContents = await getContentByAsyncBlocks(asyncBlocks); + + return tocContents + .reduce((tocGroupContent, tocContent, index) => { + const { id, type } = asyncBlocks[index]; + const groupContent = { + id, + type, + text: 'Untitled Group', + }; + + tocGroupContent.push( + !tocContent.flat(Infinity).filter(Boolean).length + ? groupContent + : tocContent + ); + + return tocGroupContent; + }, []) + .flat(Infinity) + .filter(Boolean); +}; + +export { getPageContentById }; diff --git a/libs/components/editor-core/src/editor/editor.ts b/libs/components/editor-core/src/editor/editor.ts index e7f6458a5f..74243ba6aa 100644 --- a/libs/components/editor-core/src/editor/editor.ts +++ b/libs/components/editor-core/src/editor/editor.ts @@ -333,6 +333,12 @@ export class Editor implements Virgo { return await this.getBlock({ workspace: this.workspace, id: blockId }); } + async getBlockByIds(ids: string[]): Promise[]> { + return await Promise.all( + ids.map(id => this.getBlock({ workspace: this.workspace, id })) + ); + } + /** * TODO: to be optimized * get block`s dom by block`s id @@ -477,6 +483,13 @@ export class Editor implements Virgo { return await services.api.editorBlock.query(this.workspace, query); } + async queryByPageId(pageId: string) { + return await services.api.editorBlock.get({ + workspace: this.workspace, + ids: [pageId], + }); + } + /** Hooks */ public getHooks(): HooksRunner & PluginHooks { diff --git a/libs/datasource/db-service/src/services/index.ts b/libs/datasource/db-service/src/services/index.ts index 315ee84496..c39119fe68 100644 --- a/libs/datasource/db-service/src/services/index.ts +++ b/libs/datasource/db-service/src/services/index.ts @@ -5,6 +5,7 @@ import { Database } from './database'; import { EditorBlock } from './editor-block'; import { FileService } from './file'; import { PageTree } from './workspace/page-tree'; +import { TOC } from './workspace/toc'; import { UserConfig } from './workspace/user-config'; export { @@ -48,6 +49,7 @@ export interface DbServicesMap { userConfig: UserConfig; file: FileService; commentService: CommentService; + tocService: TOC; } interface RegisterDependencyConfigWithName extends RegisterDependencyConfig { @@ -76,6 +78,13 @@ const dbServiceConfig: RegisterDependencyConfigWithName[] = [ value: PageTree, dependencies: [{ token: Database }], }, + { + type: 'class', + callName: 'tocService', + token: TOC, + value: TOC, + dependencies: [{ token: Database }], + }, { type: 'class', callName: 'userConfig', diff --git a/libs/datasource/db-service/src/services/workspace/toc.ts b/libs/datasource/db-service/src/services/workspace/toc.ts new file mode 100644 index 0000000000..16bec3da18 --- /dev/null +++ b/libs/datasource/db-service/src/services/workspace/toc.ts @@ -0,0 +1,28 @@ +import { ServiceBaseClass } from '../base'; +import { ReturnUnobserve } from '../database/observer'; +import { ObserveCallback } from './page-tree'; + +export class TOC extends ServiceBaseClass { + private onActivePageChange?: () => void; + + async observe( + { workspace, pageId }: { workspace: string; pageId: string }, + callback: ObserveCallback + ): Promise { + // not only use observe, but also addChildrenListener is OK πŸŽ‰πŸŽ‰πŸŽ‰ + // const pageBlock = await this.getBlock(workspace, pageId); + // + // this.onActivePageChange?.(); + // pageBlock?.addChildrenListener('onPageChildrenChange', () => { + // callback(); + // }); + // + // this.onActivePageChange = () => pageBlock?.removeChildrenListener('onPageChildrenChange'); + + const unobserve = await this._observe(workspace, pageId, callback); + + return () => { + unobserve(); + }; + } +} From b96d6c37f75407580309123699649fcc178a57e5 Mon Sep 17 00:00:00 2001 From: mitsuha Date: Tue, 23 Aug 2022 20:07:35 +0800 Subject: [PATCH 2/6] opti: 1.adjust eventlistener; --- .../src/pages/workspace/docs/Page.tsx | 4 +- .../workspace/docs/components/toc/TOC.tsx | 45 +++++++------------ .../workspace/docs/components/toc/index.ts | 2 +- .../utils/{getPageContentById.ts => toc.ts} | 27 +++++------ .../db-service/src/services/index.ts | 9 ---- .../db-service/src/services/workspace/toc.ts | 28 ------------ 6 files changed, 34 insertions(+), 81 deletions(-) rename apps/ligo-virgo/src/pages/workspace/docs/utils/{getPageContentById.ts => toc.ts} (67%) delete mode 100644 libs/datasource/db-service/src/services/workspace/toc.ts diff --git a/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx b/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx index 6d9d2fac90..0bbcfb5d75 100644 --- a/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx +++ b/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx @@ -23,7 +23,7 @@ import { useFlag } from '@toeverything/datasource/feature-flags'; import { CollapsiblePageTree } from './collapsible-page-tree'; import { Tabs } from './components/tabs'; import { TabMap, TAB_TITLE } from './components/tabs/Tabs'; -import { TOC } from './components/toc'; +import { Toc } from './components/toc'; import { WorkspaceName } from './workspace-name'; type PageProps = { @@ -92,7 +92,7 @@ export function Page(props: PageProps) { )} {activeTab === TabMap.get(TAB_TITLE.TOC).value && ( - TOC + TOC )} 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 b2c0aa693e..513e2913a6 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,6 +1,5 @@ import { BlockEditor } from '@toeverything/components/editor-core'; import { styled } from '@toeverything/components/ui'; -import { services } from '@toeverything/datasource/db-service'; import type { ReactNode } from 'react'; import { createContext, @@ -12,12 +11,9 @@ import { import { useParams } from 'react-router'; import { BLOCK_TYPES, - getPageContentById, -} from '../../utils/getPageContentById'; - -const StyledTOC = styled('div')(() => { - return {}; -}); + getContentByAsyncBlocks, + getPageTOC, +} from '../../utils/toc'; const StyledTOCItem = styled('a')<{ type?: string; isActive?: boolean }>( ({ type, isActive }) => { @@ -120,49 +116,42 @@ const renderTOCContent = tocDataSource => { ); }; -export const TOC = (props: Props) => { +export const Toc = (props: Props) => { const { editor } = props; - const { workspace_id, page_id } = useParams(); + const { page_id } = useParams(); const [tocDataSource, setTocDataSource] = useState([]); const [activeBlockId, setActiveBlockId] = useState('blockId'); const updateTocDataSource = useCallback(async () => { - const tocDataSource = await getPageContentById(editor, page_id); + const { children = [] } = + (await editor.queryByPageId(page_id))?.[0] || {}; + const asyncBlocks = (await editor.getBlockByIds(children)) || []; + const tocDataSource = getPageTOC( + asyncBlocks, + await getContentByAsyncBlocks(asyncBlocks, updateTocDataSource) + ); + setTocDataSource(tocDataSource); }, [editor, page_id]); useEffect(() => { - (async () => await updateTocDataSource())(); + (async () => { + await updateTocDataSource(); + })(); }, [updateTocDataSource]); - useEffect(() => { - let unobserve: () => void; - const observe = async () => { - unobserve = await services.api.tocService.observe( - { workspace: workspace_id, pageId: page_id }, - updateTocDataSource - ); - }; - void observe(); - - return () => { - unobserve?.(); - }; - }, [updateTocDataSource, workspace_id]); - const onClick = async (blockId?: string) => { if (blockId === activeBlockId) { return; } - console.log(blockId); setActiveBlockId(blockId); await editor.scrollManager.scrollIntoViewByBlockId(blockId); }; return ( - {renderTOCContent(tocDataSource)} +
{renderTOCContent(tocDataSource)}
); }; diff --git a/apps/ligo-virgo/src/pages/workspace/docs/components/toc/index.ts b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/index.ts index e603af8242..1e45e9bd09 100644 --- a/apps/ligo-virgo/src/pages/workspace/docs/components/toc/index.ts +++ b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/index.ts @@ -1 +1 @@ -export { TOC } from './TOC'; +export { Toc } from './Toc'; diff --git a/apps/ligo-virgo/src/pages/workspace/docs/utils/getPageContentById.ts b/apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts similarity index 67% rename from apps/ligo-virgo/src/pages/workspace/docs/utils/getPageContentById.ts rename to apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts index b1d16cb9d6..8331b0726d 100644 --- a/apps/ligo-virgo/src/pages/workspace/docs/utils/getPageContentById.ts +++ b/apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts @@ -1,4 +1,4 @@ -import type { BlockEditor } from '@toeverything/components/editor-core'; +import { AsyncBlock } from '@toeverything/components/editor-core'; export const BLOCK_TYPES = { GROUP: 'group', @@ -7,23 +7,29 @@ export const BLOCK_TYPES = { HEADING3: 'heading3', }; -const getContentByAsyncBlocks = async (asyncBlocks = []) => { +/* 😞😞sorry, I don't know how to define unlimited dimensions array */ +const getContentByAsyncBlocks = async ( + asyncBlocks: AsyncBlock[] = [], + callback: () => void +): Promise => { /* maybe should recast it to tail recursion */ return await Promise.all( - asyncBlocks.map(async asyncBlock => { + asyncBlocks.map(async (asyncBlock: AsyncBlock) => { const asyncBlocks = await asyncBlock.children(); if (asyncBlocks?.length) { - return getContentByAsyncBlocks(asyncBlocks); + return getContentByAsyncBlocks(asyncBlocks, callback); } - const { id, type } = asyncBlock; + /* get update notice */ + asyncBlock.onUpdate(callback); + const { id, type } = asyncBlock; if (Object.values(BLOCK_TYPES).includes(type)) { const properties = await asyncBlock.getProperties(); return { - id: id, + id, type, text: properties?.text?.value?.[0]?.text || '', }; @@ -34,12 +40,7 @@ const getContentByAsyncBlocks = async (asyncBlocks = []) => { ); }; -const getPageContentById = async (editor: BlockEditor, pageId: string) => { - const { children = [] } = (await editor.queryByPageId(pageId))?.[0] || {}; - const asyncBlocks = (await editor.getBlockByIds(children)) || []; - - const tocContents = await getContentByAsyncBlocks(asyncBlocks); - +const getPageTOC = (asyncBlocks: AsyncBlock[], tocContents) => { return tocContents .reduce((tocGroupContent, tocContent, index) => { const { id, type } = asyncBlocks[index]; @@ -61,4 +62,4 @@ const getPageContentById = async (editor: BlockEditor, pageId: string) => { .filter(Boolean); }; -export { getPageContentById }; +export { getPageTOC, getContentByAsyncBlocks }; diff --git a/libs/datasource/db-service/src/services/index.ts b/libs/datasource/db-service/src/services/index.ts index c39119fe68..315ee84496 100644 --- a/libs/datasource/db-service/src/services/index.ts +++ b/libs/datasource/db-service/src/services/index.ts @@ -5,7 +5,6 @@ import { Database } from './database'; import { EditorBlock } from './editor-block'; import { FileService } from './file'; import { PageTree } from './workspace/page-tree'; -import { TOC } from './workspace/toc'; import { UserConfig } from './workspace/user-config'; export { @@ -49,7 +48,6 @@ export interface DbServicesMap { userConfig: UserConfig; file: FileService; commentService: CommentService; - tocService: TOC; } interface RegisterDependencyConfigWithName extends RegisterDependencyConfig { @@ -78,13 +76,6 @@ const dbServiceConfig: RegisterDependencyConfigWithName[] = [ value: PageTree, dependencies: [{ token: Database }], }, - { - type: 'class', - callName: 'tocService', - token: TOC, - value: TOC, - dependencies: [{ token: Database }], - }, { type: 'class', callName: 'userConfig', diff --git a/libs/datasource/db-service/src/services/workspace/toc.ts b/libs/datasource/db-service/src/services/workspace/toc.ts deleted file mode 100644 index 16bec3da18..0000000000 --- a/libs/datasource/db-service/src/services/workspace/toc.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ServiceBaseClass } from '../base'; -import { ReturnUnobserve } from '../database/observer'; -import { ObserveCallback } from './page-tree'; - -export class TOC extends ServiceBaseClass { - private onActivePageChange?: () => void; - - async observe( - { workspace, pageId }: { workspace: string; pageId: string }, - callback: ObserveCallback - ): Promise { - // not only use observe, but also addChildrenListener is OK πŸŽ‰πŸŽ‰πŸŽ‰ - // const pageBlock = await this.getBlock(workspace, pageId); - // - // this.onActivePageChange?.(); - // pageBlock?.addChildrenListener('onPageChildrenChange', () => { - // callback(); - // }); - // - // this.onActivePageChange = () => pageBlock?.removeChildrenListener('onPageChildrenChange'); - - const unobserve = await this._observe(workspace, pageId, callback); - - return () => { - unobserve(); - }; - } -} From 85838c77a101cad77941ba2c4f3b3f13ff462ca0 Mon Sep 17 00:00:00 2001 From: mitsuha Date: Tue, 23 Aug 2022 21:25:38 +0800 Subject: [PATCH 3/6] opti: 1.adjust eventlistener; --- .../workspace/docs/components/toc/TOC.tsx | 15 +++- .../src/pages/workspace/docs/utils/toc.ts | 79 +++++++++++++------ 2 files changed, 66 insertions(+), 28 deletions(-) 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 513e2913a6..be567780f7 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 @@ -11,8 +11,10 @@ import { import { useParams } from 'react-router'; import { BLOCK_TYPES, + destroyEventList, getContentByAsyncBlocks, getPageTOC, + type TocType, } from '../../utils/toc'; const StyledTOCItem = styled('a')<{ type?: string; isActive?: boolean }>( @@ -119,19 +121,24 @@ const renderTOCContent = tocDataSource => { export const Toc = (props: Props) => { const { editor } = props; const { page_id } = useParams(); - const [tocDataSource, setTocDataSource] = useState([]); - const [activeBlockId, setActiveBlockId] = useState('blockId'); + const [tocDataSource, setTocDataSource] = useState([]); + const [activeBlockId, setActiveBlockId] = useState(''); + const [blockEventListeners, setBlockEventListeners] = useState([]); const updateTocDataSource = useCallback(async () => { + destroyEventList(blockEventListeners); + const { children = [] } = (await editor.queryByPageId(page_id))?.[0] || {}; const asyncBlocks = (await editor.getBlockByIds(children)) || []; - const tocDataSource = getPageTOC( + const { tocContents, eventListeners } = await getContentByAsyncBlocks( asyncBlocks, - await getContentByAsyncBlocks(asyncBlocks, updateTocDataSource) + updateTocDataSource ); + const tocDataSource = getPageTOC(asyncBlocks, tocContents); setTocDataSource(tocDataSource); + setBlockEventListeners(eventListeners); }, [editor, page_id]); useEffect(() => { diff --git a/apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts b/apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts index 8331b0726d..2ef9a39032 100644 --- a/apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts +++ b/apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts @@ -1,5 +1,11 @@ import { AsyncBlock } from '@toeverything/components/editor-core'; +export type TocType = { + id: string; + type: string; + text: string; +}; + export const BLOCK_TYPES = { GROUP: 'group', HEADING1: 'heading1', @@ -11,36 +17,51 @@ export const BLOCK_TYPES = { const getContentByAsyncBlocks = async ( asyncBlocks: AsyncBlock[] = [], callback: () => void -): Promise => { - /* maybe should recast it to tail recursion */ - return await Promise.all( - asyncBlocks.map(async (asyncBlock: AsyncBlock) => { - const asyncBlocks = await asyncBlock.children(); +): Promise<{ + tocContents: any[]; + eventListeners: (() => void | undefined)[]; +}> => { + const eventListeners = []; - if (asyncBlocks?.length) { - return getContentByAsyncBlocks(asyncBlocks, callback); - } + const collect = async (asyncBlocks): Promise => { + /* maybe should recast it to tail recursion */ + return await Promise.all( + asyncBlocks.map(async (asyncBlock: AsyncBlock) => { + const asyncBlocks = await asyncBlock.children(); - /* get update notice */ - asyncBlock.onUpdate(callback); + if (asyncBlocks?.length) { + return collect(asyncBlocks); + } - const { id, type } = asyncBlock; - if (Object.values(BLOCK_TYPES).includes(type)) { - const properties = await asyncBlock.getProperties(); + /* get update notice */ + const destroyHandler = asyncBlock.onUpdate(callback); - return { - id, - type, - text: properties?.text?.value?.[0]?.text || '', - }; - } + /* collect destroy handlers */ + eventListeners.push(destroyHandler); - return null; - }) - ); + 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 || '', + }; + } + + return null; + }) + ); + }; + + return { + tocContents: await collect(asyncBlocks), + eventListeners, + }; }; -const getPageTOC = (asyncBlocks: AsyncBlock[], tocContents) => { +const getPageTOC = (asyncBlocks: AsyncBlock[], tocContents): TocType[] => { return tocContents .reduce((tocGroupContent, tocContent, index) => { const { id, type } = asyncBlocks[index]; @@ -62,4 +83,14 @@ const getPageTOC = (asyncBlocks: AsyncBlock[], tocContents) => { .filter(Boolean); }; -export { getPageTOC, getContentByAsyncBlocks }; +const destroyEventList = ( + eventListeners: (() => void | undefined)[] = [] +): boolean => { + for (const eventListener of eventListeners) { + eventListener?.(); + } + + return true; +}; + +export { getPageTOC, getContentByAsyncBlocks, destroyEventList }; From ab1fe668b4fb37fc3dac349c39e39d50d9f8f80a Mon Sep 17 00:00:00 2001 From: mitsuha Date: Wed, 24 Aug 2022 01:54:10 +0800 Subject: [PATCH 4/6] opti: 1.adjust eventlistener; --- .../workspace/docs/components/toc/TOC.tsx | 20 ++++++++++--- .../src/pages/workspace/docs/utils/toc.ts | 30 ++++++++++++------- 2 files changed, 35 insertions(+), 15 deletions(-) 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 be567780f7..dd12469d90 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 @@ -14,6 +14,7 @@ import { destroyEventList, getContentByAsyncBlocks, getPageTOC, + listenerMap, type TocType, } from '../../utils/toc'; @@ -123,24 +124,35 @@ export const Toc = (props: Props) => { const { page_id } = useParams(); const [tocDataSource, setTocDataSource] = useState([]); const [activeBlockId, setActiveBlockId] = useState(''); - const [blockEventListeners, setBlockEventListeners] = useState([]); const updateTocDataSource = useCallback(async () => { - destroyEventList(blockEventListeners); + /* page listener: trigger update-notice when add new group */ + const pageAsyncBlock = (await editor.getBlockByIds([page_id]))?.[0]; + if (!listenerMap.has(pageAsyncBlock.id)) { + listenerMap.set( + pageAsyncBlock.id, + pageAsyncBlock.onUpdate(updateTocDataSource) + ); + } + /* block listener: trigger update-notice when change block content */ const { children = [] } = (await editor.queryByPageId(page_id))?.[0] || {}; const asyncBlocks = (await editor.getBlockByIds(children)) || []; - const { tocContents, eventListeners } = await getContentByAsyncBlocks( + const { tocContents } = await getContentByAsyncBlocks( asyncBlocks, updateTocDataSource ); + /* toc: flat content */ const tocDataSource = getPageTOC(asyncBlocks, tocContents); setTocDataSource(tocDataSource); - setBlockEventListeners(eventListeners); + + /* remove listener when unmount component */ + return destroyEventList; }, [editor, page_id]); + /* init toc and add page/block update-listener & unmount-listener */ useEffect(() => { (async () => { await updateTocDataSource(); diff --git a/apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts b/apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts index 2ef9a39032..b9a6e5dd56 100644 --- a/apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts +++ b/apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts @@ -13,16 +13,16 @@ export const BLOCK_TYPES = { HEADING3: 'heading3', }; +/* store page/block unmount-listener */ +export const listenerMap = new Map void>(); + /* 😞😞sorry, I don't know how to define unlimited dimensions array */ const getContentByAsyncBlocks = async ( asyncBlocks: AsyncBlock[] = [], callback: () => void ): Promise<{ tocContents: any[]; - eventListeners: (() => void | undefined)[]; }> => { - const eventListeners = []; - const collect = async (asyncBlocks): Promise => { /* maybe should recast it to tail recursion */ return await Promise.all( @@ -33,11 +33,14 @@ const getContentByAsyncBlocks = async ( return collect(asyncBlocks); } - /* get update notice */ - const destroyHandler = asyncBlock.onUpdate(callback); + /* add only once event listener for every block */ + if (!listenerMap.has(asyncBlock.id)) { + /* get update notice */ + const destroyHandler = asyncBlock.onUpdate(callback); - /* collect destroy handlers */ - eventListeners.push(destroyHandler); + /* collect destroy handlers */ + listenerMap.set(asyncBlock.id, destroyHandler); + } const { id, type } = asyncBlock; if (Object.values(BLOCK_TYPES).includes(type)) { @@ -57,10 +60,14 @@ const getContentByAsyncBlocks = async ( return { tocContents: await collect(asyncBlocks), - eventListeners, }; }; +/** + * get flat toc + * @param asyncBlocks + * @param tocContents + */ const getPageTOC = (asyncBlocks: AsyncBlock[], tocContents): TocType[] => { return tocContents .reduce((tocGroupContent, tocContent, index) => { @@ -83,9 +90,10 @@ const getPageTOC = (asyncBlocks: AsyncBlock[], tocContents): TocType[] => { .filter(Boolean); }; -const destroyEventList = ( - eventListeners: (() => void | undefined)[] = [] -): boolean => { +/* destroy page/block update-listener */ +const destroyEventList = (): boolean => { + const eventListeners = listenerMap.values(); + for (const eventListener of eventListeners) { eventListener?.(); } From c4de1a606166cd7ec6e5f8c1308d98200e53738e Mon Sep 17 00:00:00 2001 From: mitsuha Date: Wed, 24 Aug 2022 09:08:12 +0800 Subject: [PATCH 5/6] opti: 1.adjust eventlistener; --- apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts b/apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts index b9a6e5dd56..9b550e9bbe 100644 --- a/apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts +++ b/apps/ligo-virgo/src/pages/workspace/docs/utils/toc.ts @@ -93,6 +93,7 @@ const getPageTOC = (asyncBlocks: AsyncBlock[], tocContents): TocType[] => { /* destroy page/block update-listener */ const destroyEventList = (): boolean => { const eventListeners = listenerMap.values(); + listenerMap.clear(); for (const eventListener of eventListeners) { eventListener?.(); From f2a21e6c3bf6a4c16dda09d405b1b2c4477c0d48 Mon Sep 17 00:00:00 2001 From: mitsuha Date: Wed, 24 Aug 2022 10:40:14 +0800 Subject: [PATCH 6/6] opti: 1.adjust eventlistener; --- apps/ligo-virgo/src/pages/workspace/docs/Page.tsx | 4 ++-- .../src/pages/workspace/docs/components/toc/TOC.tsx | 2 +- .../src/pages/workspace/docs/components/toc/index.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx b/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx index 0bbcfb5d75..6d9d2fac90 100644 --- a/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx +++ b/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx @@ -23,7 +23,7 @@ import { useFlag } from '@toeverything/datasource/feature-flags'; import { CollapsiblePageTree } from './collapsible-page-tree'; import { Tabs } from './components/tabs'; import { TabMap, TAB_TITLE } from './components/tabs/Tabs'; -import { Toc } from './components/toc'; +import { TOC } from './components/toc'; import { WorkspaceName } from './workspace-name'; type PageProps = { @@ -92,7 +92,7 @@ export function Page(props: PageProps) { )} {activeTab === TabMap.get(TAB_TITLE.TOC).value && ( - TOC + TOC )} 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 dd12469d90..a4c4fba27e 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 @@ -119,7 +119,7 @@ const renderTOCContent = tocDataSource => { ); }; -export const Toc = (props: Props) => { +export const TOC = (props: Props) => { const { editor } = props; const { page_id } = useParams(); const [tocDataSource, setTocDataSource] = useState([]); diff --git a/apps/ligo-virgo/src/pages/workspace/docs/components/toc/index.ts b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/index.ts index 1e45e9bd09..e603af8242 100644 --- a/apps/ligo-virgo/src/pages/workspace/docs/components/toc/index.ts +++ b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/index.ts @@ -1 +1 @@ -export { Toc } from './Toc'; +export { TOC } from './TOC';