client: update tocs

This commit is contained in:
fantasticit 2022-05-28 12:48:18 +08:00
parent 4e1c464615
commit 2eea71c3e5
9 changed files with 124 additions and 88 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} id="js-reader-container"> <div ref={setContainer} id="js-tocs-container">
<div className={cls(styles.editorWrap, editorWrapClassNames)} style={{ fontSize }}> <div className={cls(styles.editorWrap, editorWrapClassNames)} style={{ fontSize }}>
<div> <div id="js-reader-container">
<DataRender <DataRender
loading={docAuthLoading} loading={docAuthLoading}
loadingContent={ loadingContent={

View File

@ -9,6 +9,8 @@
} }
.contentWrap { .contentWrap {
position: relative;
z-index: 1;
flex: 1; flex: 1;
padding: 24px 24px 48px; padding: 24px 24px 48px;
overflow: auto; overflow: auto;

View File

@ -160,9 +160,9 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
/> />
</Nav> </Nav>
</Header> </Header>
<Content className={styles.contentWrap}> <div className={styles.contentWrap} id="js-tocs-container">
{content} {content}
</Content> </div>
</Layout> </Layout>
); );
}; };

View File

@ -1,12 +1,15 @@
.toc { .toc {
width: max-content; width: max-content;
max-width: 100%; max-width: 100%;
padding: 0.75rem;
margin: 0.75em 0;
background: var(--semi-color-fill-1); background: var(--semi-color-fill-1);
border-radius: 0.5rem; border-radius: 0.5rem;
opacity: 0.75; opacity: 0.75;
&.visible {
padding: 0.75rem;
margin: 0.75em 0;
}
.list { .list {
padding: 0; padding: 0;
margin: 0 0 12px; margin: 0 0 12px;

View File

@ -1,66 +1,23 @@
import { Button, Collapsible } from '@douyinfe/semi-ui';
import { NodeViewWrapper } from '@tiptap/react'; import { NodeViewWrapper } from '@tiptap/react';
import { useToggle } from 'hooks/use-toggle'; import cls from 'classnames';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import styles from './index.module.scss'; import styles from './index.module.scss';
const arrToTree = (tocs) => { const arrToTree = (tocs) => {
const data = [...tocs, { level: Infinity }]; const levels = [{ children: [] }];
const res = []; tocs.forEach(function (o) {
levels.length = o.level;
const makeChildren = (item, flattenChildren) => { levels[o.level - 1].children = levels[o.level - 1].children || [];
if (!flattenChildren.length) return; levels[o.level - 1].children.push(o);
levels[o.level] = o;
const stopAt = flattenChildren.findIndex((d) => d.level !== item.level + 1); });
return levels[0].children;
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 isEditable = editor.isEditable;
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const [visible, toggleVisible] = useToggle(true);
const maskStyle = useMemo(
() =>
visible
? {}
: {
WebkitMaskImage:
'linear-gradient(to bottom, black 0%, rgba(0, 0, 0, 1) 60%, rgba(0, 0, 0, 0.2) 80%, transparent 100%)',
},
[visible]
);
const handleUpdate = useCallback(() => { const handleUpdate = useCallback(() => {
const headings = []; const headings = [];
@ -87,12 +44,9 @@ export const TableOfContentsWrapper = ({ editor }) => {
transaction.setMeta('addToHistory', false); transaction.setMeta('addToHistory', false);
transaction.setMeta('preventUpdate', true); transaction.setMeta('preventUpdate', true);
editor.view.dispatch(transaction); editor.view.dispatch(transaction);
setItems(headings); setItems(headings);
editor.eventEmitter.emit('TableOfContents', arrToTree(headings));
return headings;
}, [editor]); }, [editor]);
useEffect(() => { useEffect(() => {
@ -101,7 +55,7 @@ export const TableOfContentsWrapper = ({ editor }) => {
} }
if (!editor.options.editable) { if (!editor.options.editable) {
editor.eventEmitter.emit('TableOfContents', arrToTree(handleUpdate())); handleUpdate();
return; return;
} }
@ -112,10 +66,15 @@ export const TableOfContentsWrapper = ({ editor }) => {
}; };
}, [editor, handleUpdate]); }, [editor, handleUpdate]);
useEffect(() => {
handleUpdate();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return ( return (
<NodeViewWrapper className={styles.toc}> <NodeViewWrapper className={cls(styles.toc, isEditable && styles.visible)}>
{isEditable ? (
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<Collapsible isOpen={visible} collapseHeight={60} style={{ ...maskStyle }}>
<ul className={styles.list}> <ul className={styles.list}>
{items.map((item, index) => ( {items.map((item, index) => (
<li key={index} className={styles.item} style={{ paddingLeft: `${item.level - 2}rem` }}> <li key={index} className={styles.item} style={{ paddingLeft: `${item.level - 2}rem` }}>
@ -123,11 +82,8 @@ export const TableOfContentsWrapper = ({ editor }) => {
</li> </li>
))} ))}
</ul> </ul>
</Collapsible>
<Button theme="light" type="tertiary" size="small" onClick={toggleVisible}>
{visible ? '收起' : '展开'}
</Button>
</div> </div>
) : null}
</NodeViewWrapper> </NodeViewWrapper>
); );
}; };

View File

@ -174,9 +174,9 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
</header> </header>
)} )}
<main ref={$mainContainer}> <main ref={$mainContainer} id={editable ? 'js-tocs-container' : ''}>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
{editor && <Tocs tocs={headings} editor={editor} />} {!isMobile && editor ? <Tocs tocs={headings} editor={editor} /> : null}
{protals} {protals}
</main> </main>

View File

@ -43,6 +43,7 @@
} }
> main { > main {
display: flex;
flex: 1; flex: 1;
overflow: auto; overflow: auto;
} }

View File

@ -1,13 +1,32 @@
.wrapper { .wrapper {
position: fixed; position: fixed;
right: 16px; right: 0;
z-index: 4;
background-color: var(--semi-color-nav-bg); background-color: var(--semi-color-nav-bg);
> header { > header {
margin-bottom: 12px; margin-bottom: 12px;
font-weight: 600;
line-height: 22px; line-height: 22px;
color: var(--main-text-color); color: var(--main-text-color);
opacity: 0;
}
&:hover {
> header {
opacity: 1;
}
}
.collapsedItem {
position: relative;
height: 2px;
background-color: #d8d8d8;
}
:global {
.semi-anchor-link-title-active {
.collapsedItem {
background-color: var(--semi-color-primary);
}
}
} }
} }

View File

@ -1,8 +1,13 @@
import { Anchor } from '@douyinfe/semi-ui'; import { IconDoubleChevronLeft, IconDoubleChevronRight } from '@douyinfe/semi-icons';
import React, { useCallback } from 'react'; import { Anchor, Button } from '@douyinfe/semi-ui';
import { useDocumentStyle, Width } from 'hooks/use-document-style';
import { useToggle } from 'hooks/use-toggle';
import React, { useCallback, useEffect } from 'react';
import { TableOfContents } from 'tiptap/core/extensions/table-of-contents';
import { findNode } from 'tiptap/prose-utils';
import { Editor } from '../react'; import { Editor } from '../react';
import style from './index.module.scss'; import styles from './index.module.scss';
interface IToc { interface IToc {
level: number; level: number;
@ -10,24 +15,74 @@ interface IToc {
text: string; text: string;
} }
const renderToc = (toc) => { const MAX_LEVEL = 6;
const Toc = ({ toc, collapsed }) => {
return ( return (
<Anchor.Link href={`#${toc.id}`} title={toc.text}> <Anchor.Link
{toc.children && toc.children.length && toc.children.map(renderToc)} href={`#${toc.id}`}
title={
collapsed ? (
<div style={{ width: 8 * (MAX_LEVEL - toc.level + 1) }} className={styles.collapsedItem}></div>
) : (
toc.text
)
}
>
{toc.children && toc.children.length
? toc.children.map((toc) => <Toc key={toc.text} toc={toc} collapsed={collapsed} />)
: null}
</Anchor.Link> </Anchor.Link>
); );
}; };
export const Tocs: React.FC<{ tocs: Array<IToc>; editor: Editor }> = ({ tocs = [], editor }) => { export const Tocs: React.FC<{ tocs: Array<IToc>; editor: Editor }> = ({ tocs = [], editor }) => {
const [hasToc, toggleHasToc] = useToggle(false);
const [collapsed, toggleCollapsed] = useToggle(true);
const { width } = useDocumentStyle();
const getContainer = useCallback(() => { const getContainer = useCallback(() => {
return document.querySelector(`#js-reader-container`); return document.querySelector(`#js-tocs-container`);
}, []); }, []);
useEffect(() => {
if (width === Width.fullWidth) {
toggleCollapsed(true);
} else {
toggleCollapsed(false);
}
}, [width, toggleCollapsed]);
useEffect(() => {
const listener = () => {
const nodes = findNode(editor, TableOfContents.name);
const hasTocNow = !!(nodes && nodes.length);
if (hasTocNow !== hasToc) {
toggleHasToc(hasTocNow);
}
};
editor.on('transaction', listener);
return () => {
editor.off('transaction', listener);
};
}, [editor, hasToc, toggleHasToc]);
return ( return (
<div className={style.wrapper}> <div className={styles.wrapper} style={{ display: hasToc ? 'block' : 'none' }}>
<header>
<Button
type="tertiary"
theme="borderless"
icon={!collapsed ? <IconDoubleChevronRight /> : <IconDoubleChevronLeft />}
onClick={toggleCollapsed}
></Button>
</header>
<main> <main>
<Anchor autoCollapse getContainer={getContainer} maxWidth={8}> <Anchor maxHeight={500} getContainer={getContainer} maxWidth={collapsed ? 56 : 180}>
{tocs.length && tocs.map(renderToc)} {tocs.length && tocs.map((toc) => <Toc key={toc.text} toc={toc} collapsed={collapsed} />)}
</Anchor> </Anchor>
</main> </main>
</div> </div>