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>
</Header>
<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>
<div id="js-reader-container">
<DataRender
loading={docAuthLoading}
loadingContent={

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,32 @@
.wrapper {
position: fixed;
right: 16px;
z-index: 4;
right: 0;
background-color: var(--semi-color-nav-bg);
> header {
margin-bottom: 12px;
font-weight: 600;
line-height: 22px;
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 React, { useCallback } from 'react';
import { IconDoubleChevronLeft, IconDoubleChevronRight } from '@douyinfe/semi-icons';
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 style from './index.module.scss';
import styles from './index.module.scss';
interface IToc {
level: number;
@ -10,24 +15,74 @@ interface IToc {
text: string;
}
const renderToc = (toc) => {
const MAX_LEVEL = 6;
const Toc = ({ toc, collapsed }) => {
return (
<Anchor.Link href={`#${toc.id}`} title={toc.text}>
{toc.children && toc.children.length && toc.children.map(renderToc)}
<Anchor.Link
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>
);
};
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(() => {
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 (
<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>
<Anchor autoCollapse getContainer={getContainer} maxWidth={8}>
{tocs.length && tocs.map(renderToc)}
<Anchor maxHeight={500} getContainer={getContainer} maxWidth={collapsed ? 56 : 180}>
{tocs.length && tocs.map((toc) => <Toc key={toc.text} toc={toc} collapsed={collapsed} />)}
</Anchor>
</main>
</div>