client: use dropdown instad of popover

pull/64/head
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,
Button,
Checkbox,
Dropdown,
Input,
Modal,
Popconfirm,
@ -24,7 +25,7 @@ import { useUser } from 'data/user';
import { event, JOIN_USER } from 'event';
import { IsOnMobile } from 'hooks/use-on-mobile';
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 {
wikiId: string;
@ -79,6 +80,71 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
[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(() => {
if (visible) {
setTimeout(() => ref.current?.focus(), 100);
@ -139,70 +205,41 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
);
})}
</AvatarGroup>
<Popover
showArrow
visible={visible}
onVisibleChange={toggleVisible}
trigger="click"
position={isMobile ? 'topRight' : 'bottomLeft'}
style={{ width: 376, maxWidth: '80vw' }}
content={
<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>
}
>
<Button theme="borderless" type="tertiary" disabled={disabled} icon={<IconUserAdd />} />
</Popover>
{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>
)}
</>
);
};

View File

@ -1,5 +1,5 @@
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 { useDocumentDetail } from 'data/document';
import { getDocumentShareURL } from 'helpers/url';
@ -28,6 +28,7 @@ export const DocumentShare: React.FC<IProps> = ({ documentId, disabled = false,
const copyable = useMemo(
() => ({
onCopy: () => Toast.success({ content: '复制文本成功' }),
successTip: '已复制',
}),
[]
);
@ -44,6 +45,73 @@ export const DocumentShare: React.FC<IProps> = ({ documentId, disabled = false,
toggleStatus({ sharePassword: isPublic ? '' : sharePassword });
}, [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(() => {
if (loading || !data) return;
setSharePassword(data.document && data.document.sharePassword);
@ -56,72 +124,42 @@ export const DocumentShare: React.FC<IProps> = ({ documentId, disabled = false,
}, [visible]);
return (
<Popover
showArrow
visible={visible}
onVisibleChange={toggleVisible}
trigger="click"
position={isMobile ? 'top' : 'bottomLeft'}
style={{ width: 376, maxWidth: '80vw' }}
content={
<div
style={{
maxHeight: '70vh',
overflow: 'auto',
}}
onClick={prevent}
>
<div style={{ textAlign: 'center' }}>
<ShareIllustration />
</div>
{isPublic ? (
<Text
ellipsis
icon={<IconLink />}
copyable={copyable}
<>
{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: 240,
width: 412,
maxWidth: '96vw',
padding: '0 24px',
}}
>
{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>
}
>
{render ? (
render({ isPublic, disabled, toggleVisible })
) : (
<Button disabled={disabled} type="primary" theme="light" onClick={toggleVisible}>
{isPublic ? '分享中' : '分享'}
</Button>
{content}
</div>
}
>
{btn}
</Dropdown>
)}
</Popover>
</>
);
};

View File

@ -1,5 +1,5 @@
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 { useDocumentStyle } from 'hooks/use-document-style';
import { IsOnMobile } from 'hooks/use-on-mobile';
@ -21,15 +21,14 @@ export const DocumentStyle = () => {
}, [setWidth]);
return (
<Popover
<Dropdown
key="style"
showArrow
trigger="click"
zIndex={1061}
position={isMobile ? 'topRight' : 'bottomLeft'}
visible={visible}
onVisibleChange={toggleVisible}
style={{ padding: 0 }}
onClickOutSide={toggleVisible}
content={
<div className={styles.wrap}>
<div className={styles.item}>
@ -50,6 +49,6 @@ export const DocumentStyle = () => {
}
>
<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 { Empty } from 'components/empty';
import { IconMessage } from 'components/icons/IconMessage';
@ -196,15 +196,13 @@ const MessageBox = () => {
{btn}
</>
) : (
<Popover
showArrow
style={{ padding: 0 }}
<Dropdown
position="bottomRight"
trigger="click"
content={<div style={{ width: 300, padding: '16px 16px 0' }}>{content}</div>}
>
{btn}
</Popover>
</Dropdown>
)}
</span>
);

View File

@ -1,5 +1,5 @@
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 { DataRender } from 'components/data-render';
import { DocumentStar } from 'components/document/star';
@ -8,10 +8,11 @@ import { IconSearch } from 'components/icons';
import { IconDocumentFill } from 'components/icons/IconDocumentFill';
import { LocaleTime } from 'components/locale-time';
import { useAsyncLoading } from 'hooks/use-async-loading';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle';
import Link from 'next/link';
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 styles from './index.module.scss';
@ -68,6 +69,8 @@ const List: React.FC<{ data: IDocument[] }> = ({ data }) => {
};
export const Search = () => {
const ref = useRef<HTMLInputElement>();
const { isMobile } = IsOnMobile.useHook();
const [visible, toggleVisible] = useToggle(false);
const [searchApi, loading] = useAsyncLoading(searchDocument, 10);
const [keyword, setKeyword] = useState('');
@ -85,6 +88,60 @@ export const Search = () => {
});
}, [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(() => {
const fn = () => {
toggleVisible(false);
@ -99,56 +156,39 @@ export const Search = () => {
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>
<Input
autofocus
placeholder={'搜索文档'}
size="large"
value={keyword}
onChange={(val) => {
setSearchDocs([]);
setKeyword(val);
}}
onEnterPress={search}
suffix={<SemiIconSearch onClick={search} style={{ cursor: 'pointer' }} />}
showClear
/>
</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>
</Modal>
{!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>
{btn}
</>
)}
</>
);
};

View File

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

View File

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

View File

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

View File

@ -4,8 +4,8 @@ import { throttle } from 'helpers/throttle';
import { useToggle } from 'hooks/use-toggle';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import scrollIntoView from 'scroll-into-view-if-needed';
import { Editor } from 'tiptap/core';
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';