client: improve toc

pull/60/head
fantasticit 2022-05-28 00:34:03 +08:00
parent c37182f0a8
commit 4e1c464615
5 changed files with 119 additions and 4 deletions

View File

@ -132,9 +132,9 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
></Nav> ></Nav>
</Header> </Header>
<Layout className={styles.contentWrap}> <Layout className={styles.contentWrap}>
<div ref={setContainer}> <div ref={setContainer} id="js-reader-container">
<div className={cls(styles.editorWrap, editorWrapClassNames)} style={{ fontSize }}> <div className={cls(styles.editorWrap, editorWrapClassNames)} style={{ fontSize }}>
<div id="js-reader-container"> <div>
<DataRender <DataRender
loading={docAuthLoading} loading={docAuthLoading}
loadingContent={ loadingContent={

View File

@ -5,6 +5,48 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import styles from './index.module.scss'; import styles from './index.module.scss';
const arrToTree = (tocs) => {
const data = [...tocs, { level: Infinity }];
const res = [];
const makeChildren = (item, flattenChildren) => {
if (!flattenChildren.length) return;
const stopAt = flattenChildren.findIndex((d) => d.level !== item.level + 1);
if (stopAt > -1) {
const children = flattenChildren.slice(0, stopAt);
item.children = children;
const remain = flattenChildren.slice(stopAt + 1);
if (remain.length) {
makeChildren(children[children.length - 1], remain);
}
} else {
item.children = flattenChildren;
}
};
let i = 0;
while (i < data.length) {
const item = data[i];
const stopAt = data.slice(i + 1).findIndex((d) => d.level !== item.level + 1);
if (stopAt > -1) {
makeChildren(item, data.slice(i + 1).slice(0, stopAt));
i += 1 + stopAt;
} else {
i += 1;
}
res.push(item);
}
return res.slice(0, -1);
};
export const TableOfContentsWrapper = ({ editor }) => { export const TableOfContentsWrapper = ({ editor }) => {
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const [visible, toggleVisible] = useToggle(true); const [visible, toggleVisible] = useToggle(true);
@ -49,11 +91,18 @@ export const TableOfContentsWrapper = ({ editor }) => {
editor.view.dispatch(transaction); editor.view.dispatch(transaction);
setItems(headings); setItems(headings);
return headings;
}, [editor]); }, [editor]);
useEffect(() => { useEffect(() => {
if (!editor) { if (!editor) {
return null; return;
}
if (!editor.options.editable) {
editor.eventEmitter.emit('TableOfContents', arrToTree(handleUpdate()));
return;
} }
editor.on('update', handleUpdate); editor.on('update', handleUpdate);

View File

@ -8,9 +8,10 @@ import { isAndroid, isIOS } from 'helpers/env';
import { useNetwork } from 'hooks/use-network'; import { useNetwork } from 'hooks/use-network';
import { IsOnMobile } from 'hooks/use-on-mobile'; import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react'; import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { Collaboration } from 'tiptap/core/extensions/collaboration'; import { Collaboration } from 'tiptap/core/extensions/collaboration';
import { CollaborationCursor } from 'tiptap/core/extensions/collaboration-cursor'; import { CollaborationCursor } from 'tiptap/core/extensions/collaboration-cursor';
import { Tocs } from 'tiptap/editor/tocs';
import { EditorContent, useEditor } from '../../react'; import { EditorContent, useEditor } from '../../react';
import { CollaborationKit } from '../kit'; import { CollaborationKit } from '../kit';
@ -70,6 +71,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
}, },
[editable, user, onTitleUpdate, hocuspocusProvider] [editable, user, onTitleUpdate, hocuspocusProvider]
); );
const [headings, setHeadings] = useState([]);
useImperativeHandle(ref, () => editor); useImperativeHandle(ref, () => editor);
@ -137,6 +139,21 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
}; };
}, [isMobile]); }, [isMobile]);
useEffect(() => {
if (!editor) return;
const collectHeadings = (headings) => {
console.log({ headings });
setHeadings(headings);
};
editor.eventEmitter.on('TableOfContents', collectHeadings);
return () => {
editor.eventEmitter.off('TableOfContents', collectHeadings);
};
}, [editor]);
return ( return (
<> <>
{(!online || status === 'disconnected') && ( {(!online || status === 'disconnected') && (
@ -159,6 +176,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
<main ref={$mainContainer}> <main ref={$mainContainer}>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
{editor && <Tocs tocs={headings} editor={editor} />}
{protals} {protals}
</main> </main>

View File

@ -0,0 +1,13 @@
.wrapper {
position: fixed;
right: 16px;
z-index: 4;
background-color: var(--semi-color-nav-bg);
> header {
margin-bottom: 12px;
font-weight: 600;
line-height: 22px;
color: var(--main-text-color);
}
}

View File

@ -0,0 +1,35 @@
import { Anchor } from '@douyinfe/semi-ui';
import React, { useCallback } from 'react';
import { Editor } from '../react';
import style from './index.module.scss';
interface IToc {
level: number;
id: string;
text: string;
}
const renderToc = (toc) => {
return (
<Anchor.Link href={`#${toc.id}`} title={toc.text}>
{toc.children && toc.children.length && toc.children.map(renderToc)}
</Anchor.Link>
);
};
export const Tocs: React.FC<{ tocs: Array<IToc>; editor: Editor }> = ({ tocs = [], editor }) => {
const getContainer = useCallback(() => {
return document.querySelector(`#js-reader-container`);
}, []);
return (
<div className={style.wrapper}>
<main>
<Anchor autoCollapse getContainer={getContainer} maxWidth={8}>
{tocs.length && tocs.map(renderToc)}
</Anchor>
</main>
</div>
);
};