diff --git a/packages/client/src/tiptap/editor/collaboration/collaboration/editor.tsx b/packages/client/src/tiptap/editor/collaboration/collaboration/editor.tsx
index 4202311b..008f5aeb 100644
--- a/packages/client/src/tiptap/editor/collaboration/collaboration/editor.tsx
+++ b/packages/client/src/tiptap/editor/collaboration/collaboration/editor.tsx
@@ -10,7 +10,7 @@ import { useDocumentStyle } from 'hooks/use-document-style';
import { useNetwork } from 'hooks/use-network';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle';
-import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
+import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { Collaboration } from 'tiptap/core/extensions/collaboration';
import { CollaborationCursor } from 'tiptap/core/extensions/collaboration-cursor';
import { Tocs } from 'tiptap/editor/tocs';
@@ -84,11 +84,11 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
},
[editable, user, onTitleUpdate, hocuspocusProvider]
);
- const [headings, setHeadings] = useState([]);
const { width, fontSize } = useDocumentStyle();
const editorWrapClassNames = useMemo(() => {
return width === 'standardWidth' ? styles.isStandardWidth : styles.isFullWidth;
}, [width]);
+ const getTocsContainer = useCallback(() => $mainContainer.current, []);
useImperativeHandle(ref, () => editor);
@@ -156,22 +156,6 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
};
}, [isMobile]);
- useEffect(() => {
- if (!editor) return;
-
- const collectHeadings = (headings) => {
- if (headings && headings.length) {
- setHeadings(headings);
- }
- };
-
- editor.eventEmitter.on('TableOfContents', collectHeadings);
-
- return () => {
- editor.eventEmitter.off('TableOfContents', collectHeadings);
- };
- }, [editor]);
-
return (
<>
{(!online || status === 'disconnected') && (
@@ -209,21 +193,16 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
)}
- {!isMobile && editor && headings.length ? (
-
-
-
- ) : null}
+
+
+
{protals}
-
- {editable && menubar && (
- $mainContainer.current}
- style={{ right: isMobile ? 16 : 100, bottom: 65 }}
- visibilityHeight={200}
- />
- )}
+ $mainContainer.current}
+ style={{ right: isMobile ? 16 : 100, bottom: 65 }}
+ visibilityHeight={200}
+ />
>
);
});
diff --git a/packages/client/src/tiptap/editor/collaboration/reader.module.scss b/packages/client/src/tiptap/editor/collaboration/reader.module.scss
new file mode 100644
index 00000000..5321c39f
--- /dev/null
+++ b/packages/client/src/tiptap/editor/collaboration/reader.module.scss
@@ -0,0 +1,33 @@
+.wrap {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ min-height: 240px;
+ overflow: hidden;
+ flex-direction: column;
+
+ > main {
+ position: relative;
+ display: flex;
+ overflow: auto;
+ flex: 1;
+ justify-content: center;
+ flex-wrap: nowrap;
+
+ .contentWrap {
+ width: 100%;
+
+ &.isStandardWidth {
+ max-width: 750px;
+ }
+
+ &.isFullWidth {
+ max-width: 100%;
+ }
+ }
+
+ .tocsWrap {
+ position: relative;
+ }
+ }
+}
diff --git a/packages/client/src/tiptap/editor/collaboration/reader.tsx b/packages/client/src/tiptap/editor/collaboration/reader.tsx
index b31963dd..a45185c1 100644
--- a/packages/client/src/tiptap/editor/collaboration/reader.tsx
+++ b/packages/client/src/tiptap/editor/collaboration/reader.tsx
@@ -1,20 +1,24 @@
+import { BackTop } from '@douyinfe/semi-ui';
+import { isMobile } from 'helpers/env';
import { safeJSONParse } from 'helpers/json';
-import React, { useMemo } from 'react';
+import React, { useCallback, useMemo, useRef } from 'react';
import { EditorContent, useEditor } from '../react';
+import { Tocs } from '../tocs';
import { CollaborationKit } from './kit';
+import styles from './reader.module.scss';
interface IProps {
content: string;
}
export const ReaderEditor: React.FC = ({ content }) => {
+ const $mainContainer = useRef();
const json = useMemo(() => {
const c = safeJSONParse(content);
const json = c.default || c;
return json;
}, [content]);
-
const editor = useEditor(
{
editable: false,
@@ -23,6 +27,23 @@ export const ReaderEditor: React.FC = ({ content }) => {
},
[json]
);
+ const getTocsContainer = useCallback(() => $mainContainer.current, []);
- return ;
+ return (
+
+
+
+
+
+
+
+
+
+
$mainContainer.current}
+ style={{ right: isMobile ? 16 : 100, bottom: 65 }}
+ visibilityHeight={200}
+ />
+
+ );
};
diff --git a/packages/client/src/tiptap/editor/tocs/index.module.scss b/packages/client/src/tiptap/editor/tocs/index.module.scss
index 226a0298..601bf435 100644
--- a/packages/client/src/tiptap/editor/tocs/index.module.scss
+++ b/packages/client/src/tiptap/editor/tocs/index.module.scss
@@ -1,38 +1,10 @@
.wrapper {
position: fixed;
- padding-top: 1rem;
+ padding-top: 2rem;
padding-right: 1rem;
padding-left: 2rem;
- > header {
- margin-bottom: 12px;
- line-height: 22px;
- color: var(--main-text-color);
- opacity: 0;
- }
-
- &:hover {
- > header {
- opacity: 1;
- }
- }
-
- .dotWrap {
- display: flex;
- flex-direction: column;
- padding-left: 12px;
-
- .dot {
- position: relative;
- width: 10px;
- height: 10px;
- cursor: pointer;
- background-color: var(--semi-color-text-3);
- border-radius: 50%;
-
- & + .dot {
- margin-top: 10px;
- }
- }
+ .dot {
+ font-size: 8px;
}
}
diff --git a/packages/client/src/tiptap/editor/tocs/index.tsx b/packages/client/src/tiptap/editor/tocs/index.tsx
index de297a00..18806e6d 100644
--- a/packages/client/src/tiptap/editor/tocs/index.tsx
+++ b/packages/client/src/tiptap/editor/tocs/index.tsx
@@ -1,20 +1,19 @@
-import { IconDoubleChevronLeft, IconDoubleChevronRight } from '@douyinfe/semi-icons';
-import { Anchor, Button, Tooltip } from '@douyinfe/semi-ui';
-import { Editor } from '@tiptap/core';
+import { Anchor, Tooltip } from '@douyinfe/semi-ui';
import { throttle } from 'helpers/throttle';
-import { flattenTree2Array } from 'helpers/tree';
-import { useDocumentStyle, Width } from 'hooks/use-document-style';
import { useToggle } from 'hooks/use-toggle';
-import React, { useCallback, useEffect } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import { TableOfContents } from 'tiptap/core/extensions/table-of-contents';
+import { Editor } from 'tiptap/editor/react';
import { findNode } from 'tiptap/prose-utils';
import styles from './index.module.scss';
+import { flattenHeadingsToTree } from './util';
-interface IToc {
+interface IHeading {
level: number;
id: string;
text: string;
+ children?: IHeading[];
}
const Toc = ({ toc, collapsed }) => {
@@ -39,93 +38,112 @@ const Toc = ({ toc, collapsed }) => {
);
};
-const FULL_WIDTH = 1200;
+const FULL_WIDTH = 1000;
-export const Tocs: React.FC<{ tocs: Array; editor: Editor }> = ({ tocs = [], editor }) => {
- const [hasToc, toggleHasToc] = useToggle(false);
+export const Tocs: React.FC<{ editor: Editor; getContainer: () => HTMLElement }> = ({ editor, getContainer }) => {
const [collapsed, toggleCollapsed] = useToggle(true);
- const { width } = useDocumentStyle();
-
- const getContainer = useCallback(() => {
- return document.querySelector(`#js-tocs-container`);
- }, []);
+ const [headings, setHeadings] = useState([]);
+ const [nestedHeadings, setNestedHeadings] = useState([]);
useEffect(() => {
- if (width === Width.fullWidth) {
- toggleCollapsed(true);
- } else {
- toggleCollapsed(false);
- }
- }, [width, toggleCollapsed]);
+ const el = getContainer();
- useEffect(() => {
- const listener = () => {
- const nodes = findNode(editor, TableOfContents.name);
- const hasTocNow = !!(nodes && nodes.length);
- if (hasTocNow !== hasToc) {
- toggleHasToc(hasTocNow);
- }
- };
+ if (!el) return;
- editor.on('transaction', listener);
-
- return () => {
- editor.off('transaction', listener);
- };
- }, [editor, hasToc, toggleHasToc]);
-
- useEffect(() => {
- const el = document.querySelector(`#js-tocs-container`) as HTMLDivElement;
const handler = throttle(() => {
toggleCollapsed(el.offsetWidth <= FULL_WIDTH);
}, 200);
handler();
-
- window.addEventListener('resize', handler);
+ const observer = new MutationObserver(handler);
+ observer.observe(el, { attributes: true, childList: true, subtree: true });
return () => {
- window.removeEventListener('resize', handler);
+ observer.disconnect();
};
- }, [toggleCollapsed]);
+ }, [getContainer, toggleCollapsed]);
+
+ const getTocs = useCallback(() => {
+ if (!editor) return;
+ const nodes = findNode(editor, TableOfContents.name);
+ if (!nodes || !nodes.length) {
+ setHeadings([]);
+ setNestedHeadings([]);
+ return;
+ }
+
+ const headings = [];
+ const transaction = editor.state.tr;
+
+ editor.state.doc.descendants((node, pos) => {
+ if (node.type.name === 'heading') {
+ const id = `heading-${headings.length + 1}`;
+
+ if (node.attrs.id !== id) {
+ transaction.setNodeMarkup(pos, undefined, {
+ ...node.attrs,
+ id,
+ });
+ }
+
+ headings.push({
+ level: node.attrs.level,
+ text: node.textContent,
+ id,
+ });
+ }
+ });
+
+ transaction.setMeta('addToHistory', false);
+ transaction.setMeta('preventUpdate', true);
+ editor.view.dispatch(transaction);
+
+ setHeadings(headings);
+ setNestedHeadings(flattenHeadingsToTree(headings));
+ }, [editor]);
+
+ useEffect(() => {
+ if (!editor) {
+ return;
+ }
+
+ editor.on('update', getTocs);
+
+ return () => {
+ editor.off('update', getTocs);
+ };
+ }, [editor, getTocs]);
+
+ useEffect(() => {
+ getTocs();
+ }, [getTocs]);
+
+ if (!headings || !headings.length) return null;
return (
-
-
- : }
- onClick={toggleCollapsed}
- >
-
-
- {collapsed ? (
-
- {flattenTree2Array(tocs).map((toc) => {
+
+
+ {collapsed
+ ? headings.map((toc) => {
return (
-
-
-
+
+ ●
+
+ }
+ />
);
- })}
-
- ) : (
-
- {tocs.length && tocs.map((toc) => )}
-
- )}
-
+ })
+ : nestedHeadings.map((toc) =>
)}
+
);
};
diff --git a/packages/client/src/tiptap/editor/tocs/util.ts b/packages/client/src/tiptap/editor/tocs/util.ts
new file mode 100644
index 00000000..d651dc22
--- /dev/null
+++ b/packages/client/src/tiptap/editor/tocs/util.ts
@@ -0,0 +1,18 @@
+export const flattenHeadingsToTree = (tocs) => {
+ const result = [];
+ const levels = [result];
+
+ tocs.forEach((o) => {
+ let offset = -1;
+ let parent = levels[o.level + offset];
+
+ while (!parent) {
+ offset -= 1;
+ parent = levels[o.level + offset];
+ }
+
+ parent.push({ ...o, children: (levels[o.level] = []) });
+ });
+
+ return result;
+};