client: use dropdown instad of popover

This commit is contained in:
fantasticit 2022-06-03 18:08:08 +08:00
parent ef8d3924b9
commit 232c818c81
9 changed files with 320 additions and 198 deletions

View File

@ -4,6 +4,7 @@ import {
AvatarGroup, AvatarGroup,
Button, Button,
Checkbox, Checkbox,
Dropdown,
Input, Input,
Modal, Modal,
Popconfirm, Popconfirm,
@ -24,7 +25,7 @@ import { useUser } from 'data/user';
import { event, JOIN_USER } from 'event'; import { event, JOIN_USER } from 'event';
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';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
interface IProps { interface IProps {
wikiId: string; wikiId: string;
@ -79,6 +80,71 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
[deleteUser] [deleteUser]
); );
const content = useMemo(
() => (
<Tabs type="line">
<TabPane tab="添加成员" itemKey="add">
<div style={{ marginTop: 16 }}>
<Input ref={ref} placeholder="输入对方用户名" value={inviteUser} onChange={setInviteUser}></Input>
<Paragraph style={{ marginTop: 16 }}>
<span style={{ verticalAlign: 'middle' }}>
<DocumentLinkCopyer wikiId={wikiId} documentId={documentId} />
</span>
</Paragraph>
<Button theme="solid" block style={{ margin: '24px 0' }} disabled={!inviteUser} onClick={handleOk}>
</Button>
</div>
</TabPane>
<TabPane tab="协作成员" itemKey="list">
<DataRender
loading={loading}
error={error}
loadingContent={<Spin />}
normalContent={() => (
<Table dataSource={users} size="small" pagination>
<Column title="用户名" dataIndex="user.name" key="name" />
<Column
title="是否可读"
dataIndex="auth.readable"
key="readable"
render={renderChecked(updateUser, 'readable')}
align="center"
/>
<Column
title="是否可编辑"
dataIndex="auth.editable"
key="editable"
render={renderChecked(updateUser, 'editable')}
align="center"
/>
<Column
title="操作"
dataIndex="operate"
key="operate"
render={(_, document) => (
<Popconfirm showArrow title="确认删除该成员?" onConfirm={() => handleDelete(document)}>
<Button type="tertiary" theme="borderless" icon={<IconDelete />} />
</Popconfirm>
)}
/>
</Table>
)}
/>
</TabPane>
</Tabs>
),
[documentId, error, handleDelete, handleOk, inviteUser, loading, updateUser, users, wikiId]
);
const btn = useMemo(
() => (
<Button theme="borderless" type="tertiary" disabled={disabled} icon={<IconUserAdd />} onClick={toggleVisible} />
),
[disabled, toggleVisible]
);
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
setTimeout(() => ref.current?.focus(), 100); setTimeout(() => ref.current?.focus(), 100);
@ -139,70 +205,41 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
); );
})} })}
</AvatarGroup> </AvatarGroup>
<Popover {isMobile ? (
showArrow <>
<Modal
centered
title="文档协作"
visible={visible}
footer={null}
onCancel={toggleVisible}
style={{ maxWidth: '96vw' }}
>
{content}
</Modal>
{btn}
</>
) : (
<Dropdown
visible={visible} visible={visible}
onVisibleChange={toggleVisible} onVisibleChange={toggleVisible}
trigger="click" trigger="click"
position={isMobile ? 'topRight' : 'bottomLeft'} position="bottomRight"
style={{ width: 376, maxWidth: '80vw' }}
content={ content={
<Tabs type="line"> <div
<TabPane tab="添加成员" itemKey="add"> style={{
<div style={{ marginTop: 16 }}> width: 412,
<Input ref={ref} placeholder="输入对方用户名" value={inviteUser} onChange={setInviteUser}></Input> maxWidth: '96vw',
<Paragraph style={{ marginTop: 16 }}> padding: '0 24px',
}}
<span style={{ verticalAlign: 'middle' }}> >
<DocumentLinkCopyer wikiId={wikiId} documentId={documentId} /> {content}
</span>
</Paragraph>
<Button theme="solid" block style={{ margin: '24px 0' }} disabled={!inviteUser} onClick={handleOk}>
</Button>
</div> </div>
</TabPane>
<TabPane tab="协作成员" itemKey="list">
<DataRender
loading={loading}
error={error}
loadingContent={<Spin />}
normalContent={() => (
<Table dataSource={users} size="small" pagination>
<Column title="用户名" dataIndex="user.name" key="name" />
<Column
title="是否可读"
dataIndex="auth.readable"
key="readable"
render={renderChecked(updateUser, 'readable')}
align="center"
/>
<Column
title="是否可编辑"
dataIndex="auth.editable"
key="editable"
render={renderChecked(updateUser, 'editable')}
align="center"
/>
<Column
title="操作"
dataIndex="operate"
key="operate"
render={(_, document) => (
<Popconfirm showArrow title="确认删除该成员?" onConfirm={() => handleDelete(document)}>
<Button type="tertiary" theme="borderless" icon={<IconDelete />} />
</Popconfirm>
)}
/>
</Table>
)}
/>
</TabPane>
</Tabs>
} }
> >
<Button theme="borderless" type="tertiary" disabled={disabled} icon={<IconUserAdd />} /> {btn}
</Popover> </Dropdown>
)}
</> </>
); );
}; };

View File

@ -1,5 +1,5 @@
import { IconLink } from '@douyinfe/semi-icons'; import { IconLink } from '@douyinfe/semi-icons';
import { Button, Input, Popover, Space, Toast, Typography } from '@douyinfe/semi-ui'; import { Button, Dropdown, Input, Modal, Space, Toast, Typography } from '@douyinfe/semi-ui';
import { isPublicDocument } from '@think/domains'; import { isPublicDocument } from '@think/domains';
import { useDocumentDetail } from 'data/document'; import { useDocumentDetail } from 'data/document';
import { getDocumentShareURL } from 'helpers/url'; import { getDocumentShareURL } from 'helpers/url';
@ -28,6 +28,7 @@ export const DocumentShare: React.FC<IProps> = ({ documentId, disabled = false,
const copyable = useMemo( const copyable = useMemo(
() => ({ () => ({
onCopy: () => Toast.success({ content: '复制文本成功' }), onCopy: () => Toast.success({ content: '复制文本成功' }),
successTip: '已复制',
}), }),
[] []
); );
@ -44,29 +45,11 @@ export const DocumentShare: React.FC<IProps> = ({ documentId, disabled = false,
toggleStatus({ sharePassword: isPublic ? '' : sharePassword }); toggleStatus({ sharePassword: isPublic ? '' : sharePassword });
}, [isPublic, sharePassword, toggleStatus]); }, [isPublic, sharePassword, toggleStatus]);
useEffect(() => { const content = useMemo(
if (loading || !data) return; () => (
setSharePassword(data.document && data.document.sharePassword);
}, [loading, data]);
useEffect(() => {
if (visible) {
setTimeout(() => ref.current?.focus(), 100);
}
}, [visible]);
return (
<Popover
showArrow
visible={visible}
onVisibleChange={toggleVisible}
trigger="click"
position={isMobile ? 'top' : 'bottomLeft'}
style={{ width: 376, maxWidth: '80vw' }}
content={
<div <div
style={{ style={{
maxHeight: '70vh', maxWidth: '96vw',
overflow: 'auto', overflow: 'auto',
}} }}
onClick={prevent} onClick={prevent}
@ -80,7 +63,7 @@ export const DocumentShare: React.FC<IProps> = ({ documentId, disabled = false,
icon={<IconLink />} icon={<IconLink />}
copyable={copyable} copyable={copyable}
style={{ style={{
width: 240, width: 280,
}} }}
> >
{shareUrl} {shareUrl}
@ -98,7 +81,7 @@ export const DocumentShare: React.FC<IProps> = ({ documentId, disabled = false,
<Text type="tertiary"> <Text type="tertiary">
{isPublic {isPublic
? '分享开启后,该页面包含的所有内容均可访问,请谨慎开启' ? '分享开启后,该页面包含的所有内容均可访问,请谨慎开启'
: ' 分享关闭后,其他人将不能继续访问该页面'} : ' 分享关闭后,非协作成员将不能继续访问该页面'}
</Text> </Text>
</div> </div>
<Space style={{ width: '100%', justifyContent: 'end', margin: '12px 0' }}> <Space style={{ width: '100%', justifyContent: 'end', margin: '12px 0' }}>
@ -113,15 +96,70 @@ export const DocumentShare: React.FC<IProps> = ({ documentId, disabled = false,
)} )}
</Space> </Space>
</div> </div>
} ),
> [copyable, handleOk, isPublic, prevent, sharePassword, shareUrl, toggleVisible, viewUrl]
{render ? ( );
const btn = useMemo(
() =>
render ? (
render({ isPublic, disabled, toggleVisible }) render({ isPublic, disabled, toggleVisible })
) : ( ) : (
<Button disabled={disabled} type="primary" theme="light" onClick={toggleVisible}> <Button disabled={disabled} type="primary" theme="light" onClick={toggleVisible}>
{isPublic ? '分享中' : '分享'} {isPublic ? '分享中' : '分享'}
</Button> </Button>
),
[disabled, isPublic, render, toggleVisible]
);
useEffect(() => {
if (loading || !data) return;
setSharePassword(data.document && data.document.sharePassword);
}, [loading, data]);
useEffect(() => {
if (visible) {
setTimeout(() => ref.current?.focus(), 100);
}
}, [visible]);
return (
<>
{isMobile ? (
<>
<Modal
centered
title="文档分享"
visible={visible}
footer={null}
onCancel={toggleVisible}
style={{ maxWidth: '96vw' }}
>
{content}
</Modal>
{btn}
</>
) : (
<Dropdown
visible={visible}
onVisibleChange={toggleVisible}
trigger="click"
position="bottomRight"
content={
<div
style={{
width: 412,
maxWidth: '96vw',
padding: '0 24px',
}}
>
{content}
</div>
}
>
{btn}
</Dropdown>
)} )}
</Popover> </>
); );
}; };

View File

@ -1,5 +1,5 @@
import { IconArticle } from '@douyinfe/semi-icons'; import { IconArticle } from '@douyinfe/semi-icons';
import { Button, Popover, Radio, RadioGroup, Slider, Typography } from '@douyinfe/semi-ui'; import { Button, Dropdown, Radio, RadioGroup, Slider, Typography } from '@douyinfe/semi-ui';
import { throttle } from 'helpers/throttle'; import { throttle } from 'helpers/throttle';
import { useDocumentStyle } from 'hooks/use-document-style'; import { useDocumentStyle } from 'hooks/use-document-style';
import { IsOnMobile } from 'hooks/use-on-mobile'; import { IsOnMobile } from 'hooks/use-on-mobile';
@ -21,15 +21,14 @@ export const DocumentStyle = () => {
}, [setWidth]); }, [setWidth]);
return ( return (
<Popover <Dropdown
key="style" key="style"
showArrow
trigger="click" trigger="click"
zIndex={1061} zIndex={1061}
position={isMobile ? 'topRight' : 'bottomLeft'} position={isMobile ? 'topRight' : 'bottomLeft'}
visible={visible} visible={visible}
onVisibleChange={toggleVisible} onVisibleChange={toggleVisible}
style={{ padding: 0 }} onClickOutSide={toggleVisible}
content={ content={
<div className={styles.wrap}> <div className={styles.wrap}>
<div className={styles.item}> <div className={styles.item}>
@ -50,6 +49,6 @@ export const DocumentStyle = () => {
} }
> >
<Button icon={<IconArticle />} theme="borderless" type="tertiary" onMouseDown={toggleVisible} /> <Button icon={<IconArticle />} theme="borderless" type="tertiary" onMouseDown={toggleVisible} />
</Popover> </Dropdown>
); );
}; };

View File

@ -1,4 +1,4 @@
import { Badge, Button, Dropdown, Modal, Pagination, Popover, TabPane, Tabs, Typography } from '@douyinfe/semi-ui'; import { Badge, Button, Dropdown, Modal, Pagination, TabPane, Tabs, Typography } from '@douyinfe/semi-ui';
import { DataRender } from 'components/data-render'; import { DataRender } from 'components/data-render';
import { Empty } from 'components/empty'; import { Empty } from 'components/empty';
import { IconMessage } from 'components/icons/IconMessage'; import { IconMessage } from 'components/icons/IconMessage';
@ -196,15 +196,13 @@ const MessageBox = () => {
{btn} {btn}
</> </>
) : ( ) : (
<Popover <Dropdown
showArrow
style={{ padding: 0 }}
position="bottomRight" position="bottomRight"
trigger="click" trigger="click"
content={<div style={{ width: 300, padding: '16px 16px 0' }}>{content}</div>} content={<div style={{ width: 300, padding: '16px 16px 0' }}>{content}</div>}
> >
{btn} {btn}
</Popover> </Dropdown>
)} )}
</span> </span>
); );

View File

@ -1,5 +1,5 @@
import { IconSearch as SemiIconSearch } from '@douyinfe/semi-icons'; import { IconSearch as SemiIconSearch } from '@douyinfe/semi-icons';
import { Button, Input, Modal, Spin, Typography } from '@douyinfe/semi-ui'; import { Button, Dropdown, Input, Modal, Spin, Typography } from '@douyinfe/semi-ui';
import { IDocument } from '@think/domains'; import { IDocument } from '@think/domains';
import { DataRender } from 'components/data-render'; import { DataRender } from 'components/data-render';
import { DocumentStar } from 'components/document/star'; import { DocumentStar } from 'components/document/star';
@ -8,10 +8,11 @@ import { IconSearch } from 'components/icons';
import { IconDocumentFill } from 'components/icons/IconDocumentFill'; import { IconDocumentFill } from 'components/icons/IconDocumentFill';
import { LocaleTime } from 'components/locale-time'; import { LocaleTime } from 'components/locale-time';
import { useAsyncLoading } from 'hooks/use-async-loading'; import { useAsyncLoading } from 'hooks/use-async-loading';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import Link from 'next/link'; import Link from 'next/link';
import Router from 'next/router'; import Router from 'next/router';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { HttpClient } from 'services/http-client'; import { HttpClient } from 'services/http-client';
import styles from './index.module.scss'; import styles from './index.module.scss';
@ -68,6 +69,8 @@ const List: React.FC<{ data: IDocument[] }> = ({ data }) => {
}; };
export const Search = () => { export const Search = () => {
const ref = useRef<HTMLInputElement>();
const { isMobile } = IsOnMobile.useHook();
const [visible, toggleVisible] = useToggle(false); const [visible, toggleVisible] = useToggle(false);
const [searchApi, loading] = useAsyncLoading(searchDocument, 10); const [searchApi, loading] = useAsyncLoading(searchDocument, 10);
const [keyword, setKeyword] = useState(''); const [keyword, setKeyword] = useState('');
@ -85,47 +88,23 @@ export const Search = () => {
}); });
}, [searchApi, keyword]); }, [searchApi, keyword]);
useEffect(() => { const onKeywordChange = useCallback((val) => {
const fn = () => { setSearchDocs([]);
toggleVisible(false); setKeyword(val);
}; }, []);
Router.events.on('routeChangeStart', fn); const content = useMemo(
() => (
return () => {
Router.events.off('routeChangeStart', fn);
};
}, [toggleVisible]);
return (
<>
<Button type="tertiary" theme="borderless" icon={<IconSearch />} onClick={toggleVisible} />
<Modal
visible={visible}
title="文档搜索"
footer={null}
onCancel={toggleVisible}
style={{
maxWidth: '96vw',
}}
bodyStyle={{
height: '68vh',
}}
>
<div style={{ paddingBottom: 24 }}> <div style={{ paddingBottom: 24 }}>
<div> <div>
<Input <Input
autofocus showClear
ref={ref}
placeholder={'搜索文档'} placeholder={'搜索文档'}
size="large"
value={keyword} value={keyword}
onChange={(val) => { onChange={onKeywordChange}
setSearchDocs([]);
setKeyword(val);
}}
onEnterPress={search} onEnterPress={search}
suffix={<SemiIconSearch onClick={search} style={{ cursor: 'pointer' }} />} suffix={<SemiIconSearch onClick={search} style={{ cursor: 'pointer' }} />}
showClear
/> />
</div> </div>
<div style={{ height: 'calc(68vh - 40px)', paddingBottom: 36, overflow: 'auto' }}> <div style={{ height: 'calc(68vh - 40px)', paddingBottom: 36, overflow: 'auto' }}>
@ -148,7 +127,68 @@ export const Search = () => {
/> />
</div> </div>
</div> </div>
),
[error, keyword, loading, onKeywordChange, search, searchDocs]
);
const btn = useMemo(
() => <Button type="tertiary" theme="borderless" icon={<IconSearch />} onClick={toggleVisible} />,
[toggleVisible]
);
useEffect(() => {
if (visible) {
setTimeout(() => ref.current?.focus(), 100);
}
}, [visible]);
useEffect(() => {
const fn = () => {
toggleVisible(false);
};
Router.events.on('routeChangeStart', fn);
return () => {
Router.events.off('routeChangeStart', fn);
};
}, [toggleVisible]);
return (
<>
{!isMobile ? (
<Dropdown
position="bottomRight"
trigger="click"
visible={visible}
onVisibleChange={toggleVisible}
content={
<div style={{ width: 360, maxWidth: '96vw', maxHeight: '70vh', overflow: 'auto', padding: '16px 16px 0' }}>
{content}
</div>
}
>
{btn}
</Dropdown>
) : (
<>
<Modal
visible={visible}
title="文档搜索"
footer={null}
onCancel={toggleVisible}
style={{
maxWidth: '96vw',
}}
bodyStyle={{
height: '68vh',
}}
>
{content}
</Modal> </Modal>
{btn}
</>
)}
</> </>
); );
}; };

View File

@ -23,6 +23,12 @@ html:focus-within {
scroll-behavior: smooth; scroll-behavior: smooth;
} }
@media (prefers-reduced-motion) {
html:focus-within {
scroll-behavior: auto;
}
}
body { body {
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;

View File

@ -194,9 +194,11 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
</div> </div>
)} )}
</div> </div>
{!isMobile && (
<div className={styles.tocsWrap}> <div className={styles.tocsWrap}>
<Tocs editor={editor} getContainer={getTocsContainer} /> <Tocs editor={editor} getContainer={getTocsContainer} />
</div> </div>
)}
{protals} {protals}
{!editable && <ImageViewer container={$mainContainer.current} />} {!editable && <ImageViewer container={$mainContainer.current} />}
</main> </main>

View File

@ -36,9 +36,11 @@ export const ReaderEditor: React.FC<IProps> = ({ content }) => {
<div className={styles.contentWrap}> <div className={styles.contentWrap}>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
</div> </div>
{!isMobile && (
<div className={styles.tocsWrap}> <div className={styles.tocsWrap}>
<Tocs editor={editor} getContainer={getTocsContainer} /> <Tocs editor={editor} getContainer={getTocsContainer} />
</div> </div>
)}
<ImageViewer container={$mainContainer.current} /> <ImageViewer container={$mainContainer.current} />
</main> </main>
<BackTop <BackTop

View File

@ -4,8 +4,8 @@ import { throttle } from 'helpers/throttle';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import scrollIntoView from 'scroll-into-view-if-needed'; import scrollIntoView from 'scroll-into-view-if-needed';
import { Editor } from 'tiptap/core';
import { TableOfContents } from 'tiptap/core/extensions/table-of-contents'; import { TableOfContents } from 'tiptap/core/extensions/table-of-contents';
import { Editor } from 'tiptap/editor/react';
import { findNode } from 'tiptap/prose-utils'; import { findNode } from 'tiptap/prose-utils';
import styles from './index.module.scss'; import styles from './index.module.scss';