mirror of https://github.com/fantasticit/think.git
client: improve tocs layout
This commit is contained in:
parent
cd31c2c765
commit
3d5cf24ee5
|
@ -16,11 +16,9 @@ interface IProps {
|
|||
user: ILoginUser;
|
||||
documentId: string;
|
||||
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 $editor = useRef<ICollaborationRefProps>();
|
||||
const mounted = useMount();
|
||||
|
@ -92,7 +90,7 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
|
|||
}, [users, currentUser, toggleMentionUsersSettingVisible]);
|
||||
|
||||
return (
|
||||
<div className={cls(styles.editorWrap, className)} style={style}>
|
||||
<div className={cls(styles.editorWrap)}>
|
||||
{mounted && (
|
||||
<CollaborationEditor
|
||||
ref={$editor}
|
||||
|
|
|
@ -28,37 +28,6 @@
|
|||
}
|
||||
|
||||
.editorWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { Divider } from 'components/divider';
|
||||
import { DocumentCollaboration } from 'components/document/collaboration';
|
||||
|
@ -14,7 +14,6 @@ import { useDocumentDetail } from 'data/document';
|
|||
import { useUser } from 'data/user';
|
||||
import { CHANGE_DOCUMENT_TITLE, event, triggerUseDocumentVersion } from 'event';
|
||||
import { triggerRefreshTocs } from 'event';
|
||||
import { useDocumentStyle } from 'hooks/use-document-style';
|
||||
import { IsOnMobile } from 'hooks/use-on-mobile';
|
||||
import { useWindowSize } from 'hooks/use-window-size';
|
||||
import { SecureDocumentIllustration } from 'illustrations/secure-document';
|
||||
|
@ -33,10 +32,6 @@ interface IProps {
|
|||
export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
||||
const { isMobile } = IsOnMobile.useHook();
|
||||
const { width: windowWith } = useWindowSize();
|
||||
const { width, fontSize } = useDocumentStyle();
|
||||
const editorWrapClassNames = useMemo(() => {
|
||||
return width === 'standardWidth' ? styles.isStandardWidth : styles.isFullWidth;
|
||||
}, [width]);
|
||||
const [title, setTitle] = useState('');
|
||||
const { user } = useUser();
|
||||
const { data: documentAndAuth, loading: docAuthLoading, error: docAuthError } = useDocumentDetail(documentId);
|
||||
|
@ -125,13 +120,7 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
|||
</div>
|
||||
)}
|
||||
{document && <Seo title={document.title} />}
|
||||
<Editor
|
||||
user={user}
|
||||
documentId={documentId}
|
||||
authority={authority}
|
||||
className={editorWrapClassNames}
|
||||
style={{ fontSize }}
|
||||
/>
|
||||
<Editor user={user} documentId={documentId} authority={authority} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -9,34 +9,7 @@
|
|||
}
|
||||
|
||||
.contentWrap {
|
||||
flex: 1;
|
||||
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 {
|
||||
|
|
|
@ -132,9 +132,6 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
|||
></Nav>
|
||||
</Header>
|
||||
<Layout className={styles.contentWrap}>
|
||||
<div ref={setContainer} id="js-tocs-container">
|
||||
<div className={cls(styles.editorWrap, editorWrapClassNames)} style={{ fontSize }}>
|
||||
<div id="js-reader-container">
|
||||
<DataRender
|
||||
loading={docAuthLoading}
|
||||
loadingContent={
|
||||
|
@ -162,28 +159,18 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
|||
type="document"
|
||||
renderInEditorPortal={renderAuthor}
|
||||
onAwarenessUpdate={triggerJoinUser}
|
||||
renderOnMount={
|
||||
<div className={styles.commentWrap}>
|
||||
<CommentEditor documentId={documentId} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!isMobile && authority && authority.editable && container && (
|
||||
{/* {!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} />
|
||||
)}
|
||||
{container && <BackTop style={{ bottom: 65, right: isMobile ? 16 : 100 }} target={() => container} />} */}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
{isMobile && <div className={styles.mobileToolbar}>{actions}</div>}
|
||||
</div>
|
||||
|
|
|
@ -9,28 +9,7 @@
|
|||
}
|
||||
|
||||
.contentWrap {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1;
|
||||
padding: 24px 24px 48px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
BackTop,
|
||||
Breadcrumb,
|
||||
Button,
|
||||
Form,
|
||||
|
@ -10,10 +9,8 @@ import {
|
|||
Typography,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
||||
import cls from 'classnames';
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { DocumentStyle } from 'components/document/style';
|
||||
import { ImageViewer } from 'components/image-viewer';
|
||||
import { LogoImage, LogoText } from 'components/logo';
|
||||
import { Seo } from 'components/seo';
|
||||
import { Theme } from 'components/theme';
|
||||
|
@ -30,7 +27,7 @@ import { CollaborationEditor } from 'tiptap/editor';
|
|||
import { Author } from '../author';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Header, Content } = Layout;
|
||||
const { Header } = Layout;
|
||||
const { Text } = Typography;
|
||||
|
||||
interface IProps {
|
||||
|
@ -99,11 +96,7 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id="js-share-document-editor-container"
|
||||
className={cls(styles.editorWrap, editorWrapClassNames)}
|
||||
style={{ fontSize }}
|
||||
>
|
||||
<>
|
||||
{data && <Seo title={data.title} />}
|
||||
{mounted && <CollaborationEditor
|
||||
menubar={false}
|
||||
|
@ -111,14 +104,10 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
|
|||
user={null}
|
||||
id={documentId}
|
||||
type="document"
|
||||
hideComment
|
||||
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])
|
||||
|
||||
|
|
|
@ -2,9 +2,11 @@ import { BackTop, Toast } from '@douyinfe/semi-ui';
|
|||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import cls from 'classnames';
|
||||
import { Banner } from 'components/banner';
|
||||
import { CommentEditor } from 'components/document/comments';
|
||||
import { LogoName } from 'components/logo';
|
||||
import { getRandomColor } from 'helpers/color';
|
||||
import { isAndroid, isIOS } from 'helpers/env';
|
||||
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';
|
||||
|
@ -21,14 +23,25 @@ import { ICollaborationEditorProps, ProviderStatus } from './type';
|
|||
|
||||
type IProps = Pick<
|
||||
ICollaborationEditorProps,
|
||||
'editable' | 'user' | 'onTitleUpdate' | 'menubar' | 'renderInEditorPortal'
|
||||
'editable' | 'user' | 'onTitleUpdate' | 'menubar' | 'renderInEditorPortal' | 'hideComment'
|
||||
> & {
|
||||
hocuspocusProvider: HocuspocusProvider;
|
||||
status: ProviderStatus;
|
||||
documentId: string;
|
||||
};
|
||||
|
||||
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 $mainContainer = useRef<HTMLDivElement>();
|
||||
const { isMobile } = IsOnMobile.useHook();
|
||||
|
@ -72,6 +85,10 @@ 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]);
|
||||
|
||||
useImperativeHandle(ref, () => editor);
|
||||
|
||||
|
@ -175,9 +192,28 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
|
|||
</header>
|
||||
)}
|
||||
|
||||
<main ref={$mainContainer} id={editable ? 'js-tocs-container' : ''}>
|
||||
<main
|
||||
ref={$mainContainer}
|
||||
id={'js-tocs-container'}
|
||||
style={{
|
||||
padding: isMobile ? '0 24px' : '0 6rem',
|
||||
}}
|
||||
>
|
||||
<div className={cls(styles.contentWrap, editorWrapClassNames)}>
|
||||
<div style={{ fontSize }}>
|
||||
<EditorContent editor={editor} />
|
||||
{!isMobile && editor && headings.length ? <Tocs tocs={headings} editor={editor} /> : null}
|
||||
</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}
|
||||
</main>
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
flex-direction: column;
|
||||
|
||||
> header {
|
||||
position: relative;
|
||||
z-index: 110;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -43,13 +42,32 @@
|
|||
}
|
||||
|
||||
> main {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
> div:first-of-type {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
.contentWrap {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ export const CollaborationEditor = forwardRef((props: ICollaborationEditorProps,
|
|||
onTitleUpdate,
|
||||
user,
|
||||
menubar,
|
||||
renderOnMount,
|
||||
hideComment,
|
||||
renderInEditorPortal,
|
||||
onAwarenessUpdate,
|
||||
} = props;
|
||||
|
@ -124,18 +124,19 @@ export const CollaborationEditor = forwardRef((props: ICollaborationEditorProps,
|
|||
normalContent={() => (
|
||||
<EditorInstance
|
||||
ref={$editor}
|
||||
documentId={documentId}
|
||||
editable={editable}
|
||||
menubar={menubar}
|
||||
hocuspocusProvider={hocuspocusProvider}
|
||||
onTitleUpdate={onTitleUpdate}
|
||||
user={user}
|
||||
status={status}
|
||||
hideComment={hideComment}
|
||||
renderInEditorPortal={renderInEditorPortal}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{loading || !!error ? null : renderOnMount}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -28,6 +28,11 @@ export interface ICollaborationEditorProps {
|
|||
*/
|
||||
user: ILoginUser | null;
|
||||
|
||||
/**
|
||||
* 是否需要评论
|
||||
*/
|
||||
hideComment?: boolean;
|
||||
|
||||
/**
|
||||
* 文档标题变动
|
||||
*/
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
.wrapper {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
background-color: var(--semi-color-nav-bg);
|
||||
padding-top: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-left: 2rem;
|
||||
|
||||
> header {
|
||||
margin-bottom: 12px;
|
||||
|
@ -18,15 +19,9 @@
|
|||
|
||||
.collapsedItem {
|
||||
position: relative;
|
||||
height: 2px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #d8d8d8;
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-anchor-link-title-active {
|
||||
.collapsedItem {
|
||||
background-color: var(--semi-color-primary);
|
||||
}
|
||||
}
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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 { throttle } from 'helpers/throttle';
|
||||
import { useDocumentStyle, Width } from 'hooks/use-document-style';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
|
@ -15,19 +16,20 @@ interface IToc {
|
|||
text: string;
|
||||
}
|
||||
|
||||
const MAX_LEVEL = 6;
|
||||
|
||||
const Toc = ({ toc, collapsed }) => {
|
||||
return (
|
||||
<Anchor.Link
|
||||
href={`#${toc.id}`}
|
||||
title={
|
||||
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
|
||||
)
|
||||
}
|
||||
style={{ paddingLeft: collapsed ? 16 : 8 }}
|
||||
>
|
||||
{toc.children && toc.children.length
|
||||
? 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 }) => {
|
||||
const [hasToc, toggleHasToc] = useToggle(false);
|
||||
const { width } = useDocumentStyle();
|
||||
const [collapsed, toggleCollapsed] = useToggle(true);
|
||||
const { width } = useDocumentStyle();
|
||||
|
||||
const getContainer = useCallback(() => {
|
||||
return document.querySelector(`#js-tocs-container`);
|
||||
|
@ -57,7 +61,6 @@ export const Tocs: React.FC<{ tocs: Array<IToc>; editor: Editor }> = ({ tocs = [
|
|||
const listener = () => {
|
||||
const nodes = findNode(editor, TableOfContents.name);
|
||||
const hasTocNow = !!(nodes && nodes.length);
|
||||
|
||||
if (hasTocNow !== hasToc) {
|
||||
toggleHasToc(hasTocNow);
|
||||
}
|
||||
|
@ -70,6 +73,19 @@ export const Tocs: React.FC<{ tocs: Array<IToc>; editor: Editor }> = ({ tocs = [
|
|||
};
|
||||
}, [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 (
|
||||
<div className={styles.wrapper} style={{ display: hasToc ? 'block' : 'none' }}>
|
||||
<header>
|
||||
|
@ -82,7 +98,7 @@ export const Tocs: React.FC<{ tocs: Array<IToc>; editor: Editor }> = ({ tocs = [
|
|||
</header>
|
||||
<main>
|
||||
<Anchor
|
||||
railTheme={'muted'}
|
||||
railTheme={collapsed ? 'muted' : 'tertiary'}
|
||||
maxHeight={'calc(100vh - 360px)'}
|
||||
getContainer={getContainer}
|
||||
maxWidth={collapsed ? 56 : 180}
|
||||
|
|
Loading…
Reference in New Issue