From 4ded78090628bb1432c1ad8c4ddea2ae1f05aaad Mon Sep 17 00:00:00 2001 From: fantasticit Date: Mon, 30 May 2022 12:15:11 +0800 Subject: [PATCH] client: intergrate tocs is editor --- .../collaboration/collaboration/editor.tsx | 41 ++--- .../editor/collaboration/reader.module.scss | 33 ++++ .../tiptap/editor/collaboration/reader.tsx | 27 ++- .../src/tiptap/editor/tocs/index.module.scss | 34 +--- .../client/src/tiptap/editor/tocs/index.tsx | 170 ++++++++++-------- .../client/src/tiptap/editor/tocs/util.ts | 18 ++ 6 files changed, 182 insertions(+), 141 deletions(-) create mode 100644 packages/client/src/tiptap/editor/collaboration/reader.module.scss create mode 100644 packages/client/src/tiptap/editor/tocs/util.ts 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 ( -
-
- -
-
- {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; +};