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 <>
visible={visible} <Modal
onVisibleChange={toggleVisible} centered
trigger="click" title="文档协作"
position={isMobile ? 'topRight' : 'bottomLeft'} visible={visible}
style={{ width: 376, maxWidth: '80vw' }} footer={null}
content={ onCancel={toggleVisible}
<Tabs type="line"> style={{ maxWidth: '96vw' }}
<TabPane tab="添加成员" itemKey="add"> >
<div style={{ marginTop: 16 }}> {content}
<Input ref={ref} placeholder="输入对方用户名" value={inviteUser} onChange={setInviteUser}></Input> </Modal>
<Paragraph style={{ marginTop: 16 }}> {btn}
</>
<span style={{ verticalAlign: 'middle' }}> ) : (
<DocumentLinkCopyer wikiId={wikiId} documentId={documentId} /> <Dropdown
</span> visible={visible}
</Paragraph> onVisibleChange={toggleVisible}
<Button theme="solid" block style={{ margin: '24px 0' }} disabled={!inviteUser} onClick={handleOk}> trigger="click"
position="bottomRight"
</Button> content={
</div> <div
</TabPane> style={{
<TabPane tab="协作成员" itemKey="list"> width: 412,
<DataRender maxWidth: '96vw',
loading={loading} padding: '0 24px',
error={error} }}
loadingContent={<Spin />} >
normalContent={() => ( {content}
<Table dataSource={users} size="small" pagination> </div>
<Column title="用户名" dataIndex="user.name" key="name" /> }
<Column >
title="是否可读" {btn}
dataIndex="auth.readable" </Dropdown>
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 />} />
</Popover>
</> </>
); );
}; };

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,6 +45,73 @@ export const DocumentShare: React.FC<IProps> = ({ documentId, disabled = false,
toggleStatus({ sharePassword: isPublic ? '' : sharePassword }); toggleStatus({ sharePassword: isPublic ? '' : sharePassword });
}, [isPublic, sharePassword, toggleStatus]); }, [isPublic, sharePassword, toggleStatus]);
const content = useMemo(
() => (
<div
style={{
maxWidth: '96vw',
overflow: 'auto',
}}
onClick={prevent}
>
<div style={{ textAlign: 'center' }}>
<ShareIllustration />
</div>
{isPublic ? (
<Text
ellipsis
icon={<IconLink />}
copyable={copyable}
style={{
width: 280,
}}
>
{shareUrl}
</Text>
) : (
<Input
ref={ref}
mode="password"
placeholder="设置访问密码"
value={sharePassword}
onChange={setSharePassword}
></Input>
)}
<div style={{ marginTop: 16 }}>
<Text type="tertiary">
{isPublic
? '分享开启后,该页面包含的所有内容均可访问,请谨慎开启'
: ' 分享关闭后,非协作成员将不能继续访问该页面'}
</Text>
</div>
<Space style={{ width: '100%', justifyContent: 'end', margin: '12px 0' }}>
<Button onClick={() => toggleVisible(false)}></Button>
<Button theme="solid" type={isPublic ? 'danger' : 'primary'} onClick={handleOk}>
{isPublic ? '关闭分享' : '开启分享'}
</Button>
{isPublic && (
<Button theme="solid" type="primary" onClick={viewUrl}>
</Button>
)}
</Space>
</div>
),
[copyable, handleOk, isPublic, prevent, sharePassword, shareUrl, toggleVisible, viewUrl]
);
const btn = useMemo(
() =>
render ? (
render({ isPublic, disabled, toggleVisible })
) : (
<Button disabled={disabled} type="primary" theme="light" onClick={toggleVisible}>
{isPublic ? '分享中' : '分享'}
</Button>
),
[disabled, isPublic, render, toggleVisible]
);
useEffect(() => { useEffect(() => {
if (loading || !data) return; if (loading || !data) return;
setSharePassword(data.document && data.document.sharePassword); setSharePassword(data.document && data.document.sharePassword);
@ -56,72 +124,42 @@ export const DocumentShare: React.FC<IProps> = ({ documentId, disabled = false,
}, [visible]); }, [visible]);
return ( return (
<Popover <>
showArrow {isMobile ? (
visible={visible} <>
onVisibleChange={toggleVisible} <Modal
trigger="click" centered
position={isMobile ? 'top' : 'bottomLeft'} title="文档分享"
style={{ width: 376, maxWidth: '80vw' }} visible={visible}
content={ footer={null}
<div onCancel={toggleVisible}
style={{ style={{ maxWidth: '96vw' }}
maxHeight: '70vh', >
overflow: 'auto', {content}
}} </Modal>
onClick={prevent} {btn}
> </>
<div style={{ textAlign: 'center' }}> ) : (
<ShareIllustration /> <Dropdown
</div> visible={visible}
{isPublic ? ( onVisibleChange={toggleVisible}
<Text trigger="click"
ellipsis position="bottomRight"
icon={<IconLink />} content={
copyable={copyable} <div
style={{ style={{
width: 240, width: 412,
maxWidth: '96vw',
padding: '0 24px',
}} }}
> >
{shareUrl} {content}
</Text> </div>
) : ( }
<Input >
ref={ref} {btn}
mode="password" </Dropdown>
placeholder="设置访问密码"
value={sharePassword}
onChange={setSharePassword}
></Input>
)}
<div style={{ marginTop: 16 }}>
<Text type="tertiary">
{isPublic
? '分享开启后,该页面包含的所有内容均可访问,请谨慎开启'
: ' 分享关闭后,其他人将不能继续访问该页面'}
</Text>
</div>
<Space style={{ width: '100%', justifyContent: 'end', margin: '12px 0' }}>
<Button onClick={() => toggleVisible(false)}></Button>
<Button theme="solid" type={isPublic ? 'danger' : 'primary'} onClick={handleOk}>
{isPublic ? '关闭分享' : '开启分享'}
</Button>
{isPublic && (
<Button theme="solid" type="primary" onClick={viewUrl}>
</Button>
)}
</Space>
</div>
}
>
{render ? (
render({ isPublic, disabled, toggleVisible })
) : (
<Button disabled={disabled} type="primary" theme="light" onClick={toggleVisible}>
{isPublic ? '分享中' : '分享'}
</Button>
)} )}
</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,6 +88,60 @@ export const Search = () => {
}); });
}, [searchApi, keyword]); }, [searchApi, keyword]);
const onKeywordChange = useCallback((val) => {
setSearchDocs([]);
setKeyword(val);
}, []);
const content = useMemo(
() => (
<div style={{ paddingBottom: 24 }}>
<div>
<Input
showClear
ref={ref}
placeholder={'搜索文档'}
value={keyword}
onChange={onKeywordChange}
onEnterPress={search}
suffix={<SemiIconSearch onClick={search} style={{ cursor: 'pointer' }} />}
/>
</div>
<div style={{ height: 'calc(68vh - 40px)', paddingBottom: 36, overflow: 'auto' }}>
<DataRender
loading={loading}
loadingContent={
<div
style={{
paddingTop: 30,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Spin />
</div>
}
error={error}
normalContent={() => <List data={searchDocs} />}
/>
</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(() => { useEffect(() => {
const fn = () => { const fn = () => {
toggleVisible(false); toggleVisible(false);
@ -99,56 +156,39 @@ export const Search = () => {
return ( return (
<> <>
<Button type="tertiary" theme="borderless" icon={<IconSearch />} onClick={toggleVisible} /> {!isMobile ? (
<Modal <Dropdown
visible={visible} position="bottomRight"
title="文档搜索" trigger="click"
footer={null} visible={visible}
onCancel={toggleVisible} onVisibleChange={toggleVisible}
style={{ content={
maxWidth: '96vw', <div style={{ width: 360, maxWidth: '96vw', maxHeight: '70vh', overflow: 'auto', padding: '16px 16px 0' }}>
}} {content}
bodyStyle={{ </div>
height: '68vh', }
}} >
> {btn}
<div style={{ paddingBottom: 24 }}> </Dropdown>
<div> ) : (
<Input <>
autofocus <Modal
placeholder={'搜索文档'} visible={visible}
size="large" title="文档搜索"
value={keyword} footer={null}
onChange={(val) => { onCancel={toggleVisible}
setSearchDocs([]); style={{
setKeyword(val); maxWidth: '96vw',
}} }}
onEnterPress={search} bodyStyle={{
suffix={<SemiIconSearch onClick={search} style={{ cursor: 'pointer' }} />} height: '68vh',
showClear }}
/> >
</div> {content}
<div style={{ height: 'calc(68vh - 40px)', paddingBottom: 36, overflow: 'auto' }}> </Modal>
<DataRender {btn}
loading={loading} </>
loadingContent={ )}
<div
style={{
paddingTop: 30,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Spin />
</div>
}
error={error}
normalContent={() => <List data={searchDocs} />}
/>
</div>
</div>
</Modal>
</> </>
); );
}; };

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>
<div className={styles.tocsWrap}> {!isMobile && (
<Tocs editor={editor} getContainer={getTocsContainer} /> <div className={styles.tocsWrap}>
</div> <Tocs editor={editor} getContainer={getTocsContainer} />
</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>
<div className={styles.tocsWrap}> {!isMobile && (
<Tocs editor={editor} getContainer={getTocsContainer} /> <div className={styles.tocsWrap}>
</div> <Tocs editor={editor} getContainer={getTocsContainer} />
</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';