client: improve tocs layout

This commit is contained in:
fantasticit 2022-05-29 22:53:42 +08:00
parent cd31c2c765
commit 3d5cf24ee5
13 changed files with 147 additions and 192 deletions

View File

@ -16,11 +16,9 @@ interface IProps {
user: ILoginUser; user: ILoginUser;
documentId: string; documentId: string;
authority: IAuthority; authority: IAuthority;
className: string;
style: React.CSSProperties;
} }
export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, authority, className, style }) => { export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, authority }) => {
const $hasShowUserSettingModal = useRef(false); const $hasShowUserSettingModal = useRef(false);
const $editor = useRef<ICollaborationRefProps>(); const $editor = useRef<ICollaborationRefProps>();
const mounted = useMount(); const mounted = useMount();
@ -92,7 +90,7 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
}, [users, currentUser, toggleMentionUsersSettingVisible]); }, [users, currentUser, toggleMentionUsersSettingVisible]);
return ( return (
<div className={cls(styles.editorWrap, className)} style={style}> <div className={cls(styles.editorWrap)}>
{mounted && ( {mounted && (
<CollaborationEditor <CollaborationEditor
ref={$editor} ref={$editor}

View File

@ -28,37 +28,6 @@
} }
.editorWrap { .editorWrap {
display: flex;
flex-direction: column;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
> div {
> main {
padding: 24px 24px 96px;
}
}
&.isStandardWidth {
> div {
> main {
> div:first-of-type {
width: 96%;
max-width: 750px;
margin: 0 auto;
}
}
}
}
&.isFullWidth {
width: 100%;
margin: 0 auto;
> div {
> header {
justify-content: flex-start;
}
}
}
} }

View File

@ -1,5 +1,5 @@
import { IconChevronLeft } from '@douyinfe/semi-icons'; import { IconChevronLeft } from '@douyinfe/semi-icons';
import { Button, Nav, Skeleton, Space, Spin, Tooltip, Typography } from '@douyinfe/semi-ui'; import { Button, Nav, Skeleton, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
import { DataRender } from 'components/data-render'; import { DataRender } from 'components/data-render';
import { Divider } from 'components/divider'; import { Divider } from 'components/divider';
import { DocumentCollaboration } from 'components/document/collaboration'; import { DocumentCollaboration } from 'components/document/collaboration';
@ -14,7 +14,6 @@ import { useDocumentDetail } from 'data/document';
import { useUser } from 'data/user'; import { useUser } from 'data/user';
import { CHANGE_DOCUMENT_TITLE, event, triggerUseDocumentVersion } from 'event'; import { CHANGE_DOCUMENT_TITLE, event, triggerUseDocumentVersion } from 'event';
import { triggerRefreshTocs } from 'event'; import { triggerRefreshTocs } from 'event';
import { useDocumentStyle } from 'hooks/use-document-style';
import { IsOnMobile } from 'hooks/use-on-mobile'; import { IsOnMobile } from 'hooks/use-on-mobile';
import { useWindowSize } from 'hooks/use-window-size'; import { useWindowSize } from 'hooks/use-window-size';
import { SecureDocumentIllustration } from 'illustrations/secure-document'; import { SecureDocumentIllustration } from 'illustrations/secure-document';
@ -33,10 +32,6 @@ interface IProps {
export const DocumentEditor: React.FC<IProps> = ({ documentId }) => { export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
const { isMobile } = IsOnMobile.useHook(); const { isMobile } = IsOnMobile.useHook();
const { width: windowWith } = useWindowSize(); const { width: windowWith } = useWindowSize();
const { width, fontSize } = useDocumentStyle();
const editorWrapClassNames = useMemo(() => {
return width === 'standardWidth' ? styles.isStandardWidth : styles.isFullWidth;
}, [width]);
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const { user } = useUser(); const { user } = useUser();
const { data: documentAndAuth, loading: docAuthLoading, error: docAuthError } = useDocumentDetail(documentId); const { data: documentAndAuth, loading: docAuthLoading, error: docAuthError } = useDocumentDetail(documentId);
@ -125,13 +120,7 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
</div> </div>
)} )}
{document && <Seo title={document.title} />} {document && <Seo title={document.title} />}
<Editor <Editor user={user} documentId={documentId} authority={authority} />
user={user}
documentId={documentId}
authority={authority}
className={editorWrapClassNames}
style={{ fontSize }}
/>
</main> </main>
</div> </div>
); );

View File

@ -9,34 +9,7 @@
} }
.contentWrap { .contentWrap {
flex: 1;
overflow: hidden; overflow: hidden;
> div {
height: 100%;
padding: 24px 16px 48px;
overflow: auto;
}
.editorWrap {
min-height: 100%;
padding-bottom: 24px;
margin: 0 auto;
&.isStandardWidth {
width: 96%;
max-width: 750px;
}
&.isFullWidth {
width: 100%;
}
}
.commentWrap {
padding: 16px 0 32px;
border-top: 1px solid var(--semi-color-border);
}
} }
.mobileToolbar { .mobileToolbar {

View File

@ -132,58 +132,45 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
></Nav> ></Nav>
</Header> </Header>
<Layout className={styles.contentWrap}> <Layout className={styles.contentWrap}>
<div ref={setContainer} id="js-tocs-container"> <DataRender
<div className={cls(styles.editorWrap, editorWrapClassNames)} style={{ fontSize }}> loading={docAuthLoading}
<div id="js-reader-container"> loadingContent={
<DataRender <div
loading={docAuthLoading} style={{
loadingContent={ minHeight: 240,
<div display: 'flex',
style={{ alignItems: 'center',
minHeight: 240, justifyContent: 'center',
display: 'flex', margin: 'auto',
alignItems: 'center', }}
justifyContent: 'center', >
margin: 'auto', <Spin />
}}
>
<Spin />
</div>
}
error={docAuthError}
normalContent={() => (
<>
<Seo title={document.title} />
{mounted && (
<CollaborationEditor
editable={false}
user={user}
id={documentId}
type="document"
renderInEditorPortal={renderAuthor}
onAwarenessUpdate={triggerJoinUser}
renderOnMount={
<div className={styles.commentWrap}>
<CommentEditor documentId={documentId} />
</div>
}
/>
)}
{!isMobile && authority && authority.editable && container && (
<BackTop style={editBtnStyle} onClick={gotoEdit} target={() => container} visibilityHeight={200}>
<IconEdit />
</BackTop>
)}
<ImageViewer containerSelector="#js-reader-container" />
{container && (
<BackTop style={{ bottom: 65, right: isMobile ? 16 : 100 }} target={() => container} />
)}
</>
)}
/>
</div> </div>
</div> }
</div> error={docAuthError}
normalContent={() => (
<>
<Seo title={document.title} />
{mounted && (
<CollaborationEditor
editable={false}
user={user}
id={documentId}
type="document"
renderInEditorPortal={renderAuthor}
onAwarenessUpdate={triggerJoinUser}
/>
)}
{/* {!isMobile && authority && authority.editable && container && (
<BackTop style={editBtnStyle} onClick={gotoEdit} target={() => container} visibilityHeight={200}>
<IconEdit />
</BackTop>
)}
<ImageViewer containerSelector="#js-reader-container" />
{container && <BackTop style={{ bottom: 65, right: isMobile ? 16 : 100 }} target={() => container} />} */}
</>
)}
/>
</Layout> </Layout>
{isMobile && <div className={styles.mobileToolbar}>{actions}</div>} {isMobile && <div className={styles.mobileToolbar}>{actions}</div>}
</div> </div>

View File

@ -9,28 +9,7 @@
} }
.contentWrap { .contentWrap {
position: relative;
z-index: 1;
flex: 1; flex: 1;
padding: 24px 24px 48px; overflow: hidden;
overflow: auto;
.editorWrap {
min-height: 100%;
padding-bottom: 24px;
&.isStandardWidth {
width: 96%;
max-width: 750px;
margin: 0 auto;
overflow: auto;
}
&.isFullWidth {
width: 100%;
margin: 0 auto;
overflow: auto;
}
}
} }
} }

View File

@ -1,5 +1,4 @@
import { import {
BackTop,
Breadcrumb, Breadcrumb,
Button, Button,
Form, Form,
@ -10,10 +9,8 @@ import {
Typography, Typography,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form'; import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import cls from 'classnames';
import { DataRender } from 'components/data-render'; import { DataRender } from 'components/data-render';
import { DocumentStyle } from 'components/document/style'; import { DocumentStyle } from 'components/document/style';
import { ImageViewer } from 'components/image-viewer';
import { LogoImage, LogoText } from 'components/logo'; import { LogoImage, LogoText } from 'components/logo';
import { Seo } from 'components/seo'; import { Seo } from 'components/seo';
import { Theme } from 'components/theme'; import { Theme } from 'components/theme';
@ -30,7 +27,7 @@ import { CollaborationEditor } from 'tiptap/editor';
import { Author } from '../author'; import { Author } from '../author';
import styles from './index.module.scss'; import styles from './index.module.scss';
const { Header, Content } = Layout; const { Header } = Layout;
const { Text } = Typography; const { Text } = Typography;
interface IProps { interface IProps {
@ -99,11 +96,7 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
} }
return ( return (
<div <>
id="js-share-document-editor-container"
className={cls(styles.editorWrap, editorWrapClassNames)}
style={{ fontSize }}
>
{data && <Seo title={data.title} />} {data && <Seo title={data.title} />}
{mounted && <CollaborationEditor {mounted && <CollaborationEditor
menubar={false} menubar={false}
@ -111,14 +104,10 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
user={null} user={null}
id={documentId} id={documentId}
type="document" type="document"
hideComment
renderInEditorPortal={renderAuthor} renderInEditorPortal={renderAuthor}
/>} />}
<ImageViewer containerSelector="#js-share-document-editor-container" /> </>
<BackTop
style={{ bottom: 65, right: isMobile ? 16 : 100 }}
target={() => document.querySelector('#js-share-document-editor-container').parentNode}
/>
</div>
) )
}, [error, data, mounted, editorWrapClassNames, fontSize]) }, [error, data, mounted, editorWrapClassNames, fontSize])

View File

@ -2,9 +2,11 @@ import { BackTop, Toast } from '@douyinfe/semi-ui';
import { HocuspocusProvider } from '@hocuspocus/provider'; import { HocuspocusProvider } from '@hocuspocus/provider';
import cls from 'classnames'; import cls from 'classnames';
import { Banner } from 'components/banner'; import { Banner } from 'components/banner';
import { CommentEditor } from 'components/document/comments';
import { LogoName } from 'components/logo'; import { LogoName } from 'components/logo';
import { getRandomColor } from 'helpers/color'; import { getRandomColor } from 'helpers/color';
import { isAndroid, isIOS } from 'helpers/env'; import { isAndroid, isIOS } from 'helpers/env';
import { useDocumentStyle } from 'hooks/use-document-style';
import { useNetwork } from 'hooks/use-network'; import { useNetwork } from 'hooks/use-network';
import { IsOnMobile } from 'hooks/use-on-mobile'; import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
@ -21,14 +23,25 @@ import { ICollaborationEditorProps, ProviderStatus } from './type';
type IProps = Pick< type IProps = Pick<
ICollaborationEditorProps, ICollaborationEditorProps,
'editable' | 'user' | 'onTitleUpdate' | 'menubar' | 'renderInEditorPortal' 'editable' | 'user' | 'onTitleUpdate' | 'menubar' | 'renderInEditorPortal' | 'hideComment'
> & { > & {
hocuspocusProvider: HocuspocusProvider; hocuspocusProvider: HocuspocusProvider;
status: ProviderStatus; status: ProviderStatus;
documentId: string;
}; };
export const EditorInstance = forwardRef((props: IProps, ref) => { export const EditorInstance = forwardRef((props: IProps, ref) => {
const { hocuspocusProvider, editable, user, onTitleUpdate, status, menubar, renderInEditorPortal } = props; const {
hocuspocusProvider,
documentId,
editable,
user,
hideComment,
status,
menubar,
renderInEditorPortal,
onTitleUpdate,
} = props;
const $headerContainer = useRef<HTMLDivElement>(); const $headerContainer = useRef<HTMLDivElement>();
const $mainContainer = useRef<HTMLDivElement>(); const $mainContainer = useRef<HTMLDivElement>();
const { isMobile } = IsOnMobile.useHook(); const { isMobile } = IsOnMobile.useHook();
@ -72,6 +85,10 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
[editable, user, onTitleUpdate, hocuspocusProvider] [editable, user, onTitleUpdate, hocuspocusProvider]
); );
const [headings, setHeadings] = useState([]); const [headings, setHeadings] = useState([]);
const { width, fontSize } = useDocumentStyle();
const editorWrapClassNames = useMemo(() => {
return width === 'standardWidth' ? styles.isStandardWidth : styles.isFullWidth;
}, [width]);
useImperativeHandle(ref, () => editor); useImperativeHandle(ref, () => editor);
@ -175,9 +192,28 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
</header> </header>
)} )}
<main ref={$mainContainer} id={editable ? 'js-tocs-container' : ''}> <main
<EditorContent editor={editor} /> ref={$mainContainer}
{!isMobile && editor && headings.length ? <Tocs tocs={headings} editor={editor} /> : null} id={'js-tocs-container'}
style={{
padding: isMobile ? '0 24px' : '0 6rem',
}}
>
<div className={cls(styles.contentWrap, editorWrapClassNames)}>
<div style={{ fontSize }}>
<EditorContent editor={editor} />
</div>
{!editable && !hideComment && (
<div className={styles.commentWrap}>
<CommentEditor documentId={documentId} />
</div>
)}
</div>
{!isMobile && editor && headings.length ? (
<div className={styles.tocsWrap}>
<Tocs tocs={headings} editor={editor} />
</div>
) : null}
{protals} {protals}
</main> </main>

View File

@ -7,7 +7,6 @@
flex-direction: column; flex-direction: column;
> header { > header {
position: relative;
z-index: 110; z-index: 110;
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -43,13 +42,32 @@
} }
> main { > main {
position: relative;
display: flex; display: flex;
flex: 1;
overflow: auto; overflow: auto;
flex: 1;
justify-content: center;
flex-wrap: nowrap;
> div:first-of-type { .contentWrap {
flex: 1;
width: 100%; width: 100%;
&.isStandardWidth {
max-width: 750px;
}
&.isFullWidth {
max-width: 100%;
}
.commentWrap {
padding: 16px 0 32px;
border-top: 1px solid var(--semi-color-border);
}
}
.tocsWrap {
position: relative;
} }
} }
} }

View File

@ -26,7 +26,7 @@ export const CollaborationEditor = forwardRef((props: ICollaborationEditorProps,
onTitleUpdate, onTitleUpdate,
user, user,
menubar, menubar,
renderOnMount, hideComment,
renderInEditorPortal, renderInEditorPortal,
onAwarenessUpdate, onAwarenessUpdate,
} = props; } = props;
@ -124,18 +124,19 @@ export const CollaborationEditor = forwardRef((props: ICollaborationEditorProps,
normalContent={() => ( normalContent={() => (
<EditorInstance <EditorInstance
ref={$editor} ref={$editor}
documentId={documentId}
editable={editable} editable={editable}
menubar={menubar} menubar={menubar}
hocuspocusProvider={hocuspocusProvider} hocuspocusProvider={hocuspocusProvider}
onTitleUpdate={onTitleUpdate} onTitleUpdate={onTitleUpdate}
user={user} user={user}
status={status} status={status}
hideComment={hideComment}
renderInEditorPortal={renderInEditorPortal} renderInEditorPortal={renderInEditorPortal}
/> />
)} )}
/> />
</div> </div>
{loading || !!error ? null : renderOnMount}
</> </>
); );
}); });

View File

@ -28,6 +28,11 @@ export interface ICollaborationEditorProps {
*/ */
user: ILoginUser | null; user: ILoginUser | null;
/**
*
*/
hideComment?: boolean;
/** /**
* *
*/ */

View File

@ -1,7 +1,8 @@
.wrapper { .wrapper {
position: fixed; position: fixed;
right: 0; padding-top: 1rem;
background-color: var(--semi-color-nav-bg); padding-right: 1rem;
padding-left: 2rem;
> header { > header {
margin-bottom: 12px; margin-bottom: 12px;
@ -18,15 +19,9 @@
.collapsedItem { .collapsedItem {
position: relative; position: relative;
height: 2px; width: 10px;
height: 10px;
background-color: #d8d8d8; background-color: #d8d8d8;
} border-radius: 50%;
:global {
.semi-anchor-link-title-active {
.collapsedItem {
background-color: var(--semi-color-primary);
}
}
} }
} }

View File

@ -1,6 +1,7 @@
import { IconDoubleChevronLeft, IconDoubleChevronRight } from '@douyinfe/semi-icons'; import { IconDoubleChevronLeft, IconDoubleChevronRight } from '@douyinfe/semi-icons';
import { Anchor, Button } from '@douyinfe/semi-ui'; import { Anchor, Button, Tooltip } from '@douyinfe/semi-ui';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
import { throttle } from 'helpers/throttle';
import { useDocumentStyle, Width } from 'hooks/use-document-style'; import { useDocumentStyle, Width } from 'hooks/use-document-style';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
@ -15,19 +16,20 @@ interface IToc {
text: string; text: string;
} }
const MAX_LEVEL = 6;
const Toc = ({ toc, collapsed }) => { const Toc = ({ toc, collapsed }) => {
return ( return (
<Anchor.Link <Anchor.Link
href={`#${toc.id}`} href={`#${toc.id}`}
title={ title={
collapsed ? ( collapsed ? (
<div style={{ width: 8 * (MAX_LEVEL - toc.level + 1) }} className={styles.collapsedItem}></div> <Tooltip content={toc.text} position="right">
<div className={styles.collapsedItem}></div>
</Tooltip>
) : ( ) : (
toc.text toc.text
) )
} }
style={{ paddingLeft: collapsed ? 16 : 8 }}
> >
{toc.children && toc.children.length {toc.children && toc.children.length
? toc.children.map((toc) => <Toc key={toc.text} toc={toc} collapsed={collapsed} />) ? toc.children.map((toc) => <Toc key={toc.text} toc={toc} collapsed={collapsed} />)
@ -36,10 +38,12 @@ const Toc = ({ toc, collapsed }) => {
); );
}; };
const FULL_WIDTH = 1200;
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 [hasToc, toggleHasToc] = useToggle(false);
const { width } = useDocumentStyle();
const [collapsed, toggleCollapsed] = useToggle(true); const [collapsed, toggleCollapsed] = useToggle(true);
const { width } = useDocumentStyle();
const getContainer = useCallback(() => { const getContainer = useCallback(() => {
return document.querySelector(`#js-tocs-container`); return document.querySelector(`#js-tocs-container`);
@ -57,7 +61,6 @@ export const Tocs: React.FC<{ tocs: Array<IToc>; editor: Editor }> = ({ tocs = [
const listener = () => { const listener = () => {
const nodes = findNode(editor, TableOfContents.name); const nodes = findNode(editor, TableOfContents.name);
const hasTocNow = !!(nodes && nodes.length); const hasTocNow = !!(nodes && nodes.length);
if (hasTocNow !== hasToc) { if (hasTocNow !== hasToc) {
toggleHasToc(hasTocNow); toggleHasToc(hasTocNow);
} }
@ -70,6 +73,19 @@ export const Tocs: React.FC<{ tocs: Array<IToc>; editor: Editor }> = ({ tocs = [
}; };
}, [editor, hasToc, toggleHasToc]); }, [editor, hasToc, toggleHasToc]);
useEffect(() => {
const el = document.querySelector(`#js-tocs-container`) as HTMLDivElement;
const handler = throttle(() => {
toggleCollapsed(el.offsetWidth <= FULL_WIDTH);
}, 200);
window.addEventListener('resize', handler);
return () => {
window.removeEventListener('resize', handler);
};
}, [toggleCollapsed]);
return ( return (
<div className={styles.wrapper} style={{ display: hasToc ? 'block' : 'none' }}> <div className={styles.wrapper} style={{ display: hasToc ? 'block' : 'none' }}>
<header> <header>
@ -82,7 +98,7 @@ export const Tocs: React.FC<{ tocs: Array<IToc>; editor: Editor }> = ({ tocs = [
</header> </header>
<main> <main>
<Anchor <Anchor
railTheme={'muted'} railTheme={collapsed ? 'muted' : 'tertiary'}
maxHeight={'calc(100vh - 360px)'} maxHeight={'calc(100vh - 360px)'}
getContainer={getContainer} getContainer={getContainer}
maxWidth={collapsed ? 56 : 180} maxWidth={collapsed ? 56 : 180}