mirror of https://github.com/fantasticit/think.git
client: update tocs
parent
4e1c464615
commit
2eea71c3e5
|
@ -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={
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
}
|
||||
|
||||
.contentWrap {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1;
|
||||
padding: 24px 24px 48px;
|
||||
overflow: auto;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Collapsible isOpen={visible} collapseHeight={60} style={{ ...maskStyle }}>
|
||||
<NodeViewWrapper className={cls(styles.toc, isEditable && styles.visible)}>
|
||||
{isEditable ? (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<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>
|
||||
</div>
|
||||
) : null}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
}
|
||||
|
||||
> main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue