mirror of https://github.com/fantasticit/think.git
client: update tocs
This commit is contained in:
parent
4e1c464615
commit
2eea71c3e5
|
@ -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={
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
> main {
|
> main {
|
||||||
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue