mirror of https://github.com/fantasticit/think.git
client: improve toc
parent
c37182f0a8
commit
4e1c464615
|
@ -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={
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue