mirror of https://github.com/fantasticit/think.git
refactor: improve tiptap
This commit is contained in:
parent
a85132b30c
commit
8cbfbd71d1
|
@ -83,6 +83,7 @@
|
|||
"react-split-pane": "^0.1.92",
|
||||
"scroll-into-view-if-needed": "^2.2.29",
|
||||
"swr": "^1.2.0",
|
||||
"tilg": "^0.1.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"toggle-selection": "^1.0.6",
|
||||
"viewerjs": "^1.10.4",
|
||||
|
|
|
@ -69,8 +69,9 @@ export const DataRender: React.FC<IProps> = ({
|
|||
return (
|
||||
<LoadingWrap
|
||||
loading={loading}
|
||||
loadingContent={runRender(loadingContent)}
|
||||
normalContent={loading ? null : runRender(normalContent)}
|
||||
runRender={runRender}
|
||||
loadingContent={loadingContent}
|
||||
normalContent={loading ? null : normalContent}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
|
||||
interface IProps {
|
||||
loading: boolean;
|
||||
delay?: number;
|
||||
loadingContent: React.ReactElement;
|
||||
normalContent: React.ReactElement;
|
||||
}
|
||||
// interface IProps {
|
||||
// loading: boolean;
|
||||
// delay?: number;
|
||||
// runRender
|
||||
// loadingContent: React.ReactElement;
|
||||
// normalContent: React.ReactElement;
|
||||
// }
|
||||
|
||||
export const LoadingWrap: React.FC<IProps> = ({ loading, delay = 200, loadingContent, normalContent }) => {
|
||||
export const LoadingWrap = ({ loading, delay = 200, runRender, loadingContent, normalContent }) => {
|
||||
const timer = useRef<ReturnType<typeof setTimeout>>(null);
|
||||
const [showLoading, toggleShowLoading] = useToggle(false);
|
||||
|
||||
|
@ -31,8 +32,8 @@ export const LoadingWrap: React.FC<IProps> = ({ loading, delay = 200, loadingCon
|
|||
}, [delay, loading, toggleShowLoading]);
|
||||
|
||||
if (loading) {
|
||||
return showLoading ? loadingContent : null;
|
||||
return showLoading ? runRender(loadingContent) : null;
|
||||
}
|
||||
|
||||
return normalContent;
|
||||
return runRender(normalContent);
|
||||
};
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import { useEditor, EditorContent } from '@tiptap/react';
|
||||
import { Avatar, Button, Space, Typography, Banner, Pagination } from '@douyinfe/semi-ui';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { CommentKit, Document, History, CommentMenuBar } from 'tiptap';
|
||||
import { CommentKit, CommentMenuBar, useEditor, EditorContent } from 'tiptap/editor';
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { useUser } from 'data/user';
|
||||
import { useComments } from 'data/comment';
|
||||
|
@ -34,7 +33,7 @@ export const CommentEditor: React.FC<IProps> = ({ documentId }) => {
|
|||
|
||||
const editor = useEditor({
|
||||
editable: true,
|
||||
extensions: [...CommentKit, Document, History],
|
||||
extensions: CommentKit,
|
||||
});
|
||||
|
||||
const openEditor = () => {
|
||||
|
|
|
@ -1,33 +1,12 @@
|
|||
import Router from 'next/router';
|
||||
import React, { useMemo, useEffect, useState, useRef } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import cls from 'classnames';
|
||||
import deepEqual from 'deep-equal';
|
||||
import { BackTop, Toast, Spin, Typography } from '@douyinfe/semi-ui';
|
||||
import { ILoginUser, IAuthority } from '@think/domains';
|
||||
import { SecureDocumentIllustration } from 'illustrations/secure-document';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { useNetwork } from 'hooks/use-network';
|
||||
import {
|
||||
useEditor,
|
||||
EditorContent,
|
||||
MenuBar,
|
||||
BaseKit,
|
||||
DocumentWithTitle,
|
||||
getCollaborationExtension,
|
||||
getCollaborationCursorExtension,
|
||||
getProvider,
|
||||
destoryProvider,
|
||||
ProviderStatus,
|
||||
getIndexdbProvider,
|
||||
destoryIndexdbProvider,
|
||||
} from 'tiptap';
|
||||
import { findMentions } from 'tiptap/prose-utils';
|
||||
import { useCollaborationDocument } from 'data/document';
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { Banner } from 'components/banner';
|
||||
import { LogoName } from 'components/logo';
|
||||
import { debounce } from 'helpers/debounce';
|
||||
import { event, triggerChangeDocumentTitle, triggerJoinUser, USE_DOCUMENT_VERSION } from 'event';
|
||||
import { CollaborationEditor, ICollaborationRefProps } from 'tiptap/editor';
|
||||
import { DocumentUserSetting } from './users';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
@ -39,93 +18,31 @@ interface IProps {
|
|||
style: React.CSSProperties;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const _Editor: React.FC<IProps> = ({ user: currentUser, documentId, authority, className, style }) => {
|
||||
export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, authority, className, style }) => {
|
||||
const $hasShowUserSettingModal = useRef(false);
|
||||
const $editor = useRef<ICollaborationRefProps>();
|
||||
const { users, addUser, updateUser } = useCollaborationDocument(documentId);
|
||||
const [status, setStatus] = useState<ProviderStatus>('connecting');
|
||||
const { online } = useNetwork();
|
||||
const [loading, toggleLoading] = useToggle(true);
|
||||
const [error, setError] = useState(null);
|
||||
const provider = useMemo(() => {
|
||||
return getProvider({
|
||||
targetId: documentId,
|
||||
token: currentUser.token,
|
||||
cacheType: 'EDITOR',
|
||||
user: currentUser,
|
||||
docType: 'document',
|
||||
events: {
|
||||
onAwarenessUpdate({ states }) {
|
||||
triggerJoinUser(states);
|
||||
},
|
||||
onAuthenticationFailed() {
|
||||
toggleLoading(false);
|
||||
setError(new Error('鉴权失败!暂时无法提供服务'));
|
||||
},
|
||||
onSynced() {
|
||||
toggleLoading(false);
|
||||
},
|
||||
onStatus({ status }) {
|
||||
setStatus(status);
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [documentId, currentUser, toggleLoading]);
|
||||
const editor = useEditor(
|
||||
{
|
||||
editable: authority && authority.editable,
|
||||
extensions: [
|
||||
...BaseKit,
|
||||
DocumentWithTitle,
|
||||
getCollaborationExtension(provider),
|
||||
getCollaborationCursorExtension(provider, currentUser),
|
||||
],
|
||||
onTransaction: debounce(({ transaction }) => {
|
||||
try {
|
||||
const title = transaction.doc.content.firstChild.content.firstChild.textContent;
|
||||
triggerChangeDocumentTitle(title);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}, 50),
|
||||
onDestroy() {
|
||||
destoryProvider(provider, 'EDITOR');
|
||||
},
|
||||
},
|
||||
[authority, provider]
|
||||
);
|
||||
const [mentionUsersSettingVisible, toggleMentionUsersSettingVisible] = useToggle(false);
|
||||
const [mentionUsers, setMentionUsers] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authority || !authority.editable) return;
|
||||
|
||||
const indexdbProvider = getIndexdbProvider(documentId, provider.document);
|
||||
|
||||
indexdbProvider.on('synced', () => {
|
||||
setStatus('loadCacheSuccess');
|
||||
});
|
||||
|
||||
return () => {
|
||||
destoryIndexdbProvider(documentId);
|
||||
};
|
||||
}, [documentId, provider, authority]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (data) => {
|
||||
const editor = $editor.current && $editor.current.getEditor();
|
||||
if (!editor) return;
|
||||
const handler = (data) => editor.commands.setContent(data);
|
||||
editor.commands.setContent(data);
|
||||
};
|
||||
event.on(USE_DOCUMENT_VERSION, handler);
|
||||
|
||||
return () => {
|
||||
event.off(USE_DOCUMENT_VERSION, handler);
|
||||
};
|
||||
}, [editor]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
const editor = $editor.current && $editor.current.getEditor();
|
||||
if (!editor) return;
|
||||
|
||||
const handler = () => {
|
||||
// 已经拦截过一次,不再拦截
|
||||
if ($hasShowUserSettingModal.current) return;
|
||||
|
||||
|
@ -169,75 +86,20 @@ export const _Editor: React.FC<IProps> = ({ user: currentUser, documentId, autho
|
|||
Router.events.off('routeChangeStart', handler);
|
||||
window.removeEventListener('unload', handler);
|
||||
};
|
||||
}, [editor, users, currentUser, toggleMentionUsersSettingVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (event: KeyboardEvent) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.keyCode == 83) {
|
||||
event.preventDefault();
|
||||
Toast.info(`${LogoName}会实时保存你的数据,无需手动保存。`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
window.document.addEventListener('keydown', listener);
|
||||
|
||||
return () => {
|
||||
window.document.removeEventListener('keydown', listener);
|
||||
};
|
||||
}, []);
|
||||
}, [users, currentUser, toggleMentionUsersSettingVisible]);
|
||||
|
||||
return (
|
||||
<DataRender
|
||||
loading={loading}
|
||||
loadingContent={
|
||||
<div style={{ margin: '10vh auto' }}>
|
||||
<Spin tip="正在为您加载编辑器中...">
|
||||
{/* FIXME: semi-design 的问题,不加 div,文字会换行! */}
|
||||
<div></div>
|
||||
</Spin>
|
||||
</div>
|
||||
}
|
||||
error={error}
|
||||
errorContent={(error) => (
|
||||
<div
|
||||
style={{
|
||||
margin: '10vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<SecureDocumentIllustration />
|
||||
<Text style={{ marginTop: 12 }} type="danger">
|
||||
{(error && error.message) || '未知错误'}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<div className={styles.editorWrap}>
|
||||
{(!online || status === 'disconnected') && (
|
||||
<Banner
|
||||
type="warning"
|
||||
description="我们已与您断开连接,您可以继续编辑文档。一旦重新连接,我们会自动重新提交数据。"
|
||||
<div className={cls(styles.editorWrap, className)} style={style}>
|
||||
<CollaborationEditor
|
||||
ref={$editor}
|
||||
menubar
|
||||
editable={authority && authority.editable}
|
||||
user={currentUser}
|
||||
id={documentId}
|
||||
type="document"
|
||||
onTitleUpdate={triggerChangeDocumentTitle}
|
||||
onAwarenessUpdate={triggerJoinUser}
|
||||
/>
|
||||
)}
|
||||
{authority && !authority.editable && (
|
||||
<Banner type="warning" description="您没有编辑权限,暂不能编辑该文档。" closeable={false} />
|
||||
)}
|
||||
<header className={className}>
|
||||
<div>
|
||||
<MenuBar editor={editor} />
|
||||
</div>
|
||||
</header>
|
||||
<main id="js-template-editor-container" style={style}>
|
||||
<div className={cls(styles.contentWrap, className)}>
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
<BackTop target={() => document.querySelector('#js-template-editor-container')} />
|
||||
</main>
|
||||
<DocumentUserSetting
|
||||
visible={mentionUsersSettingVisible}
|
||||
toggleVisible={toggleMentionUsersSettingVisible}
|
||||
|
@ -248,19 +110,4 @@ export const _Editor: React.FC<IProps> = ({ user: currentUser, documentId, autho
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Editor = React.memo(_Editor, (prevProps, nextProps) => {
|
||||
if (deepEqual(prevProps, nextProps)) return true;
|
||||
Toast.info({
|
||||
content: '信息已更新,我们将为您重新加载页面!',
|
||||
duration: 3,
|
||||
onClose() {
|
||||
Router.reload();
|
||||
},
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* stylelint-disable no-descending-specificity */
|
||||
.wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -30,51 +31,31 @@
|
|||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
> header {
|
||||
position: relative;
|
||||
z-index: 110;
|
||||
display: flex;
|
||||
height: 50px;
|
||||
padding: 0 24px;
|
||||
overflow: hidden;
|
||||
background-color: var(--semi-color-nav-bg);
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--semi-color-border);
|
||||
user-select: none;
|
||||
|
||||
&.isStandardWidth {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.isFullWidth {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> main {
|
||||
flex: 1;
|
||||
height: calc(100% - 50px);
|
||||
overflow: auto;
|
||||
|
||||
.contentWrap {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,7 +70,12 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
|||
<Skeleton active placeholder={<Skeleton.Title style={{ width: 80 }} />} loading={true} />
|
||||
}
|
||||
normalContent={() => (
|
||||
<Text ellipsis={{ showTooltip: true }} style={{ width: ~~(windowWith / 4) }}>
|
||||
<Text
|
||||
ellipsis={{
|
||||
showTooltip: { opts: { content: title, style: { wordBreak: 'break-all' } } },
|
||||
}}
|
||||
style={{ width: ~~(windowWith / 4) }}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
import React, { useMemo, useEffect, useState } from 'react';
|
||||
import { Layout, Spin, Typography } from '@douyinfe/semi-ui';
|
||||
import { IDocument, ILoginUser } from '@think/domains';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import {
|
||||
useEditor,
|
||||
EditorContent,
|
||||
BaseKit,
|
||||
DocumentWithTitle,
|
||||
getCollaborationExtension,
|
||||
getCollaborationCursorExtension,
|
||||
getProvider,
|
||||
destoryProvider,
|
||||
} from 'tiptap';
|
||||
import { SecureDocumentIllustration } from 'illustrations/secure-document';
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { ImageViewer } from 'components/image-viewer';
|
||||
import { triggerJoinUser } from 'event';
|
||||
import { CreateUser } from './user';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Content } = Layout;
|
||||
const { Text } = Typography;
|
||||
|
||||
interface IProps {
|
||||
user: ILoginUser;
|
||||
documentId: string;
|
||||
document: IDocument;
|
||||
}
|
||||
|
||||
export const Editor: React.FC<IProps> = ({ user, documentId, document, children }) => {
|
||||
const [loading, toggleLoading] = useToggle(true);
|
||||
const [error, setError] = useState(null);
|
||||
const provider = useMemo(() => {
|
||||
return getProvider({
|
||||
targetId: documentId,
|
||||
token: user.token,
|
||||
cacheType: 'READER',
|
||||
user,
|
||||
docType: 'document',
|
||||
events: {
|
||||
onAwarenessUpdate({ states }) {
|
||||
triggerJoinUser(states);
|
||||
},
|
||||
onAuthenticationFailed() {
|
||||
toggleLoading(false);
|
||||
setError(new Error('鉴权失败!暂时无法提供服务'));
|
||||
},
|
||||
onSynced() {
|
||||
toggleLoading(false);
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [documentId, user, toggleLoading]);
|
||||
const editor = useEditor({
|
||||
editable: false,
|
||||
extensions: [
|
||||
...BaseKit,
|
||||
DocumentWithTitle,
|
||||
getCollaborationExtension(provider),
|
||||
getCollaborationCursorExtension(provider, user),
|
||||
],
|
||||
editorProps: {
|
||||
// @ts-ignore
|
||||
taskItemClickable: true,
|
||||
},
|
||||
onDestroy() {
|
||||
destoryProvider(provider, 'READER');
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<DataRender
|
||||
loading={loading}
|
||||
loadingContent={
|
||||
<div style={{ margin: '10vh auto' }}>
|
||||
<Spin tip="正在为您加载文档内容中...">
|
||||
{/* FIXME: semi-design 的问题,不加 div,文字会换行! */}
|
||||
<div />
|
||||
</Spin>
|
||||
</div>
|
||||
}
|
||||
error={error}
|
||||
errorContent={(error) => (
|
||||
<div
|
||||
style={{
|
||||
margin: '10vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<SecureDocumentIllustration />
|
||||
<Text style={{ marginTop: 12 }} type="danger">
|
||||
{(error && error.message) || '未知错误'}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<Content className={styles.editorWrap}>
|
||||
<div id="js-reader-container">
|
||||
<ImageViewer containerSelector="#js-reader-container" />
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
<CreateUser
|
||||
document={document}
|
||||
container={() => window.document.querySelector('#js-reader-container .ProseMirror .title')}
|
||||
/>
|
||||
{children}
|
||||
</Content>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,8 +1,22 @@
|
|||
import Router from 'next/router';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import cls from 'classnames';
|
||||
import { Layout, Nav, Space, Button, Typography, Skeleton, Tooltip, Popover, BackTop, Spin } from '@douyinfe/semi-ui';
|
||||
import { IconEdit, IconArticle } from '@douyinfe/semi-icons';
|
||||
import {
|
||||
Layout,
|
||||
Nav,
|
||||
Space,
|
||||
Avatar,
|
||||
Button,
|
||||
Typography,
|
||||
Skeleton,
|
||||
Tooltip,
|
||||
Popover,
|
||||
BackTop,
|
||||
Spin,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { LocaleTime } from 'components/locale-time';
|
||||
import { IconUser, IconEdit, IconArticle } from '@douyinfe/semi-icons';
|
||||
import { Seo } from 'components/seo';
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { DocumentShare } from 'components/document/share';
|
||||
|
@ -15,11 +29,24 @@ import { useDocumentStyle } from 'hooks/use-document-style';
|
|||
import { useWindowSize } from 'hooks/use-window-size';
|
||||
import { useUser } from 'data/user';
|
||||
import { useDocumentDetail } from 'data/document';
|
||||
import { Editor } from './editor';
|
||||
import { triggerJoinUser } from 'event';
|
||||
import { CollaborationEditor } from 'tiptap/editor';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Header } = Layout;
|
||||
const { Text } = Typography;
|
||||
const EditBtnStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: 30,
|
||||
width: 30,
|
||||
borderRadius: '100%',
|
||||
backgroundColor: '#0077fa',
|
||||
color: '#fff',
|
||||
bottom: 100,
|
||||
transform: 'translateY(-50px)',
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
documentId: string;
|
||||
|
@ -36,6 +63,51 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
|||
const { data: documentAndAuth, loading: docAuthLoading, error: docAuthError } = useDocumentDetail(documentId);
|
||||
const { document, authority } = documentAndAuth || {};
|
||||
|
||||
const renderAuthor = useCallback(
|
||||
(element) => {
|
||||
if (!document) return null;
|
||||
|
||||
const target = element && element.querySelector('.ProseMirror .title');
|
||||
|
||||
if (target) {
|
||||
return createPortal(
|
||||
<div
|
||||
style={{
|
||||
borderTop: '1px solid var(--semi-color-border)',
|
||||
marginTop: 24,
|
||||
padding: '16px 0',
|
||||
fontSize: 13,
|
||||
fontWeight: 'normal',
|
||||
color: 'var(--semi-color-text-0)',
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
<Avatar size="small" src={document.createUser && document.createUser.avatar}>
|
||||
<IconUser />
|
||||
</Avatar>
|
||||
<div>
|
||||
<p style={{ margin: 0 }}>
|
||||
创建者:
|
||||
{document.createUser && document.createUser.name}
|
||||
</p>
|
||||
<p style={{ margin: '8px 0 0' }}>
|
||||
最近更新日期:
|
||||
<LocaleTime date={document.updatedAt} timeago />
|
||||
{' ⦁ '}阅读量:
|
||||
{document.views}
|
||||
</p>
|
||||
</div>
|
||||
</Space>
|
||||
</div>,
|
||||
target
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[document]
|
||||
);
|
||||
|
||||
const gotoEdit = useCallback(() => {
|
||||
Router.push(`/wiki/${document.wikiId}/document/${document.id}/edit`);
|
||||
}, [document]);
|
||||
|
@ -54,7 +126,13 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
|||
error={docAuthError}
|
||||
loadingContent={<Skeleton active placeholder={<Skeleton.Title style={{ width: 80 }} />} loading={true} />}
|
||||
normalContent={() => (
|
||||
<Text strong ellipsis={{ showTooltip: true }} style={{ width: ~~(windowWidth / 4) }}>
|
||||
<Text
|
||||
strong
|
||||
ellipsis={{
|
||||
showTooltip: { opts: { content: document.title, style: { wordBreak: 'break-all' } } },
|
||||
}}
|
||||
style={{ width: ~~(windowWidth / 4) }}
|
||||
>
|
||||
{document.title}
|
||||
</Text>
|
||||
)}
|
||||
|
@ -103,30 +181,22 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
|||
<>
|
||||
<Seo title={document.title} />
|
||||
{user && (
|
||||
<Editor key={document.id} user={user} documentId={document.id} document={document}>
|
||||
<CollaborationEditor
|
||||
editable={false}
|
||||
user={user}
|
||||
id={documentId}
|
||||
type="document"
|
||||
renderInEditorPortal={renderAuthor}
|
||||
onAwarenessUpdate={triggerJoinUser}
|
||||
/>
|
||||
)}
|
||||
{user && (
|
||||
<div className={styles.commentWrap}>
|
||||
<CommentEditor documentId={document.id} />
|
||||
</div>
|
||||
</Editor>
|
||||
)}
|
||||
{authority && authority.editable && container && (
|
||||
<BackTop
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: 30,
|
||||
width: 30,
|
||||
borderRadius: '100%',
|
||||
backgroundColor: '#0077fa',
|
||||
color: '#fff',
|
||||
bottom: 100,
|
||||
transform: `translateY(-50px);`,
|
||||
}}
|
||||
onClick={gotoEdit}
|
||||
target={() => container}
|
||||
visibilityHeight={200}
|
||||
>
|
||||
<BackTop style={EditBtnStyle} onClick={gotoEdit} target={() => container} visibilityHeight={200}>
|
||||
<IconEdit />
|
||||
</BackTop>
|
||||
)}
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { IDocument } from '@think/domains';
|
||||
import { useEditor, EditorContent, BaseKit, DocumentWithTitle } from 'tiptap';
|
||||
import { safeJSONParse } from 'helpers/json';
|
||||
import { CreateUser } from '../user';
|
||||
|
||||
interface IProps {
|
||||
document: IDocument;
|
||||
createUserContainerSelector: string;
|
||||
}
|
||||
|
||||
export const DocumentContent: React.FC<IProps> = ({ document, createUserContainerSelector }) => {
|
||||
const json = useMemo(() => {
|
||||
const c = safeJSONParse(document.content);
|
||||
const json = c.default || c;
|
||||
return json;
|
||||
}, [document]);
|
||||
|
||||
const editor = useEditor(
|
||||
{
|
||||
editable: false,
|
||||
extensions: [...BaseKit, DocumentWithTitle],
|
||||
content: json,
|
||||
},
|
||||
[json]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditorContent editor={editor} />
|
||||
<CreateUser document={document} container={() => window.document.querySelector(createUserContainerSelector)} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -24,8 +24,8 @@ import { Theme } from 'components/theme';
|
|||
import { ImageViewer } from 'components/image-viewer';
|
||||
import { useDocumentStyle } from 'hooks/use-document-style';
|
||||
import { usePublicDocument } from 'data/document';
|
||||
import { DocumentSkeleton } from 'tiptap';
|
||||
import { DocumentContent } from './content';
|
||||
import { DocumentSkeleton } from 'tiptap/components/skeleton';
|
||||
import { CollaborationEditor } from 'tiptap/editor';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Header, Content } = Layout;
|
||||
|
@ -121,26 +121,21 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
|
|||
}}
|
||||
loadingContent={
|
||||
<div className={cls(styles.editorWrap, editorWrapClassNames)} style={{ fontSize }}>
|
||||
<DocumentSkeleton />
|
||||
1<DocumentSkeleton />
|
||||
</div>
|
||||
}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<>
|
||||
<Seo title={data.title} />
|
||||
<div
|
||||
id="js-share-document-editor-container"
|
||||
className={cls(styles.editorWrap, editorWrapClassNames)}
|
||||
style={{ fontSize }}
|
||||
id="js-share-document-editor-container"
|
||||
>
|
||||
<DocumentContent
|
||||
document={data}
|
||||
createUserContainerSelector="#js-share-document-editor-container .ProseMirror .title"
|
||||
/>
|
||||
</div>
|
||||
<Seo title={data.title} />
|
||||
<CollaborationEditor menubar={false} editable={false} user={null} id={documentId} type="document" />
|
||||
<ImageViewer containerSelector="#js-share-document-editor-container" />
|
||||
<BackTop target={() => document.querySelector('#js-share-document-editor-container').parentNode} />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
import { createPortal } from 'react-dom';
|
||||
import { Space, Avatar } from '@douyinfe/semi-ui';
|
||||
import { IconUser } from '@douyinfe/semi-icons';
|
||||
import { IDocument } from '@think/domains';
|
||||
import { LocaleTime } from 'components/locale-time';
|
||||
|
||||
export const CreateUser: React.FC<{ document: IDocument; container: () => HTMLElement }> = ({
|
||||
document,
|
||||
container = null,
|
||||
}) => {
|
||||
if (!document.createUser) return null;
|
||||
|
||||
const content = (
|
||||
<div
|
||||
style={{
|
||||
borderTop: '1px solid var(--semi-color-border)',
|
||||
marginTop: 24,
|
||||
padding: '16px 0',
|
||||
fontSize: 13,
|
||||
fontWeight: 'normal',
|
||||
color: 'var(--semi-color-text-0)',
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
<Avatar size="small" src={document.createUser && document.createUser.avatar}>
|
||||
<IconUser />
|
||||
</Avatar>
|
||||
<div>
|
||||
<p style={{ margin: 0 }}>
|
||||
创建者:
|
||||
{document.createUser && document.createUser.name}
|
||||
</p>
|
||||
<p style={{ margin: '8px 0 0' }}>
|
||||
最近更新日期:
|
||||
<LocaleTime date={document.updatedAt} timeago />
|
||||
{' ⦁ '}阅读量:
|
||||
{document.views}
|
||||
</p>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
|
||||
const el = container && container();
|
||||
|
||||
if (!el) return content;
|
||||
return createPortal(content, el);
|
||||
};
|
|
@ -3,7 +3,7 @@ import { Button, Modal, Typography } from '@douyinfe/semi-ui';
|
|||
import { IconChevronLeft } from '@douyinfe/semi-icons';
|
||||
import { useEditor, EditorContent } from '@tiptap/react';
|
||||
import cls from 'classnames';
|
||||
import { BaseKit, DocumentWithTitle } from 'tiptap';
|
||||
import { CollaborationKit } from 'tiptap/editor';
|
||||
import { safeJSONParse } from 'helpers/json';
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { LocaleTime } from 'components/locale-time';
|
||||
|
@ -25,7 +25,7 @@ export const DocumentVersion: React.FC<IProps> = ({ documentId, onSelect }) => {
|
|||
|
||||
const editor = useEditor({
|
||||
editable: false,
|
||||
extensions: [...BaseKit, DocumentWithTitle],
|
||||
extensions: CollaborationKit,
|
||||
content: { type: 'doc', content: [] },
|
||||
});
|
||||
|
||||
|
|
|
@ -1,35 +1,15 @@
|
|||
import React, { useMemo, useCallback, useState, useEffect } from 'react';
|
||||
import Router from 'next/router';
|
||||
import cls from 'classnames';
|
||||
import {
|
||||
Button,
|
||||
Nav,
|
||||
Space,
|
||||
Typography,
|
||||
Tooltip,
|
||||
Switch,
|
||||
Popover,
|
||||
Popconfirm,
|
||||
BackTop,
|
||||
Toast,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { Button, Nav, Space, Typography, Tooltip, Switch, Popover, Popconfirm } from '@douyinfe/semi-ui';
|
||||
import { IconChevronLeft, IconArticle } from '@douyinfe/semi-icons';
|
||||
import { ILoginUser, ITemplate } from '@think/domains';
|
||||
import { Theme } from 'components/theme';
|
||||
import {
|
||||
useEditor,
|
||||
EditorContent,
|
||||
BaseKit,
|
||||
DocumentWithTitle,
|
||||
getCollaborationExtension,
|
||||
getProvider,
|
||||
MenuBar,
|
||||
} from 'tiptap';
|
||||
import { User } from 'components/user';
|
||||
import { DocumentStyle } from 'components/document/style';
|
||||
import { LogoName } from 'components/logo';
|
||||
import { useDocumentStyle } from 'hooks/use-document-style';
|
||||
import { useWindowSize } from 'hooks/use-window-size';
|
||||
import { CollaborationEditor } from 'tiptap/editor';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
@ -44,30 +24,6 @@ interface IProps {
|
|||
export const Editor: React.FC<IProps> = ({ user, data, updateTemplate, deleteTemplate }) => {
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
const [title, setTitle] = useState(data.title);
|
||||
const provider = useMemo(() => {
|
||||
return getProvider({
|
||||
targetId: data.id,
|
||||
token: user.token,
|
||||
cacheType: 'READER',
|
||||
user,
|
||||
docType: 'template',
|
||||
});
|
||||
}, []);
|
||||
const editor = useEditor(
|
||||
{
|
||||
editable: true,
|
||||
extensions: [...BaseKit, DocumentWithTitle, getCollaborationExtension(provider)],
|
||||
onTransaction: ({ transaction }) => {
|
||||
try {
|
||||
const title = transaction.doc.content.firstChild.content.firstChild.textContent;
|
||||
setTitle(title);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
},
|
||||
},
|
||||
[provider]
|
||||
);
|
||||
const [isPublic, setPublic] = useState(false);
|
||||
const { width, fontSize } = useDocumentStyle();
|
||||
const editorWrapClassNames = useMemo(() => {
|
||||
|
@ -89,22 +45,6 @@ export const Editor: React.FC<IProps> = ({ user, data, updateTemplate, deleteTem
|
|||
setPublic(data.isPublic);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (event: KeyboardEvent) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.keyCode == 83) {
|
||||
event.preventDefault();
|
||||
Toast.info(`${LogoName}会实时保存你的数据,无需手动保存。`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
window.document.addEventListener('keydown', listener);
|
||||
|
||||
return () => {
|
||||
window.document.removeEventListener('keydown', listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<header>
|
||||
|
@ -140,17 +80,9 @@ export const Editor: React.FC<IProps> = ({ user, data, updateTemplate, deleteTem
|
|||
</header>
|
||||
<main className={styles.contentWrap}>
|
||||
<div className={styles.editorWrap}>
|
||||
<header className={editorWrapClassNames}>
|
||||
<div>
|
||||
<MenuBar editor={editor} />
|
||||
</div>
|
||||
</header>
|
||||
<main id="js-template-editor-container">
|
||||
<div className={cls(styles.contentWrap, editorWrapClassNames)} style={{ fontSize }}>
|
||||
<EditorContent editor={editor} />
|
||||
<CollaborationEditor menubar editable user={user} id={data.id} type="template" onTitleUpdate={setTitle} />
|
||||
</div>
|
||||
<BackTop target={() => document.querySelector('#js-template-editor-container')} />
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* stylelint-disable no-descending-specificity */
|
||||
.wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -30,55 +31,33 @@
|
|||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
> header {
|
||||
position: relative;
|
||||
z-index: 110;
|
||||
display: flex;
|
||||
height: 50px;
|
||||
padding: 0 24px;
|
||||
overflow: hidden;
|
||||
background-color: var(--semi-color-nav-bg);
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--semi-color-border);
|
||||
user-select: none;
|
||||
|
||||
> div {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&.isStandardWidth {
|
||||
> div {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.isFullWidth {
|
||||
> div {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> main {
|
||||
flex: 1;
|
||||
height: calc(100% - 50px);
|
||||
overflow: auto;
|
||||
|
||||
.contentWrap {
|
||||
> div:first-of-type > main {
|
||||
padding: 24px 24px 96px;
|
||||
}
|
||||
}
|
||||
|
||||
&.isStandardWidth {
|
||||
.isStandardWidth {
|
||||
> div {
|
||||
> main {
|
||||
> div:first-of-type {
|
||||
width: 96%;
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.isFullWidth {
|
||||
.isFullWidth {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
> div {
|
||||
> header {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
import { useState, useEffect, DependencyList } from 'react';
|
||||
import { EditorOptions } from '@tiptap/core';
|
||||
import { Editor } from '@tiptap/react';
|
||||
|
||||
function useForceUpdate() {
|
||||
const [, setValue] = useState(0);
|
||||
|
||||
return () => setValue((value) => value + 1);
|
||||
}
|
||||
|
||||
export const useEditor = (options: Partial<EditorOptions> = {}, deps: DependencyList = []) => {
|
||||
const [editor, setEditor] = useState<Editor | null>(null);
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
useEffect(() => {
|
||||
const instance = new Editor(options);
|
||||
|
||||
setEditor(instance);
|
||||
|
||||
// instance.on('transaction', () => {
|
||||
// requestAnimationFrame(() => {
|
||||
// requestAnimationFrame(() => {
|
||||
// console.log('update');
|
||||
// forceUpdate();
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
return () => {
|
||||
instance.destroy();
|
||||
};
|
||||
}, deps);
|
||||
|
||||
return editor;
|
||||
};
|
|
@ -1,79 +0,0 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import cls from 'classnames';
|
||||
import { Layout, Spin, Typography } from '@douyinfe/semi-ui';
|
||||
import { IUser, ITemplate } from '@think/domains';
|
||||
import { useEditor, EditorContent, BaseKit, DocumentWithTitle } from 'tiptap';
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { ImageViewer } from 'components/image-viewer';
|
||||
import { useDocumentStyle } from 'hooks/use-document-style';
|
||||
import { safeJSONParse } from 'helpers/json';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Content } = Layout;
|
||||
const { Title } = Typography;
|
||||
|
||||
interface IProps {
|
||||
user: IUser;
|
||||
data: ITemplate;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export const Editor: React.FC<IProps> = ({ user, data, loading, error }) => {
|
||||
const json = useMemo(() => {
|
||||
const c = safeJSONParse(data.content);
|
||||
let json = c.default || c;
|
||||
|
||||
if (json && json.content) {
|
||||
json = {
|
||||
type: 'doc',
|
||||
content: json.content.slice(1),
|
||||
};
|
||||
}
|
||||
|
||||
return json;
|
||||
}, [data]);
|
||||
|
||||
const editor = useEditor(
|
||||
{
|
||||
editable: false,
|
||||
extensions: [...BaseKit, DocumentWithTitle],
|
||||
content: json,
|
||||
},
|
||||
[json]
|
||||
);
|
||||
|
||||
const { width, fontSize } = useDocumentStyle();
|
||||
const editorWrapClassNames = useMemo(() => {
|
||||
return width === 'standardWidth' ? styles.isStandardWidth : styles.isFullWidth;
|
||||
}, [width]);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<Layout className={styles.contentWrap}>
|
||||
<DataRender
|
||||
loading={false}
|
||||
loadingContent={
|
||||
<div style={{ margin: 24 }}>
|
||||
<Spin></Spin>
|
||||
</div>
|
||||
}
|
||||
error={error}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<Content className={cls(styles.editorWrap)}>
|
||||
<div className={editorWrapClassNames} style={{ fontSize }}>
|
||||
<Title>{data.title}</Title>
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
<ImageViewer containerSelector={`.${styles.editorWrap}`} />
|
||||
</Content>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,32 +0,0 @@
|
|||
.wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.contentWrap {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.editorWrap {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.isStandardWidth {
|
||||
width: 96%;
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.isFullWidth {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
|
@ -1,17 +1,16 @@
|
|||
import React from 'react';
|
||||
import { Spin } from '@douyinfe/semi-ui';
|
||||
import { useUser } from 'data/user';
|
||||
import { Seo } from 'components/seo';
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { useTemplate } from 'data/template';
|
||||
import { Editor } from './editor';
|
||||
import { ImageViewer } from 'components/image-viewer';
|
||||
import { ReaderEditor } from 'tiptap/editor';
|
||||
|
||||
interface IProps {
|
||||
templateId: string;
|
||||
}
|
||||
|
||||
export const TemplateReader: React.FC<IProps> = ({ templateId }) => {
|
||||
const { user } = useUser();
|
||||
const { data, loading, error } = useTemplate(templateId);
|
||||
|
||||
return (
|
||||
|
@ -25,9 +24,10 @@ export const TemplateReader: React.FC<IProps> = ({ templateId }) => {
|
|||
error={error}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<div style={{ fontSize: 16 }}>
|
||||
<div id="js-template-reader" className="container">
|
||||
<Seo title={data.title} />
|
||||
<Editor user={user} data={data} loading={loading} error={error} />
|
||||
<ReaderEditor content={data.content} />
|
||||
<ImageViewer containerSelector={`#js-template-reader`} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -75,7 +75,7 @@ export const Tree = ({ data, docAsLink, getDocLink, parentIds, activeId, isShare
|
|||
<a className={styles.left}>
|
||||
<Typography.Text
|
||||
ellipsis={{
|
||||
showTooltip: { opts: { content: label, position: 'right' } },
|
||||
showTooltip: { opts: { content: label, style: { wordBreak: 'break-all' }, position: 'right' } },
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
|
|
|
@ -29,7 +29,7 @@ export const useComments = (documentId) => {
|
|||
mutate();
|
||||
return ret;
|
||||
},
|
||||
[mutate]
|
||||
[mutate, documentId]
|
||||
);
|
||||
|
||||
const updateComment = useCallback(
|
||||
|
@ -41,7 +41,7 @@ export const useComments = (documentId) => {
|
|||
mutate();
|
||||
return ret;
|
||||
},
|
||||
[mutate]
|
||||
[mutate, documentId]
|
||||
);
|
||||
|
||||
const deleteComment = useCallback(
|
||||
|
|
|
@ -59,7 +59,7 @@ export const useDocumentDetail = (documentId, options = null) => {
|
|||
mutate();
|
||||
return res;
|
||||
},
|
||||
[mutate]
|
||||
[mutate, documentId]
|
||||
);
|
||||
|
||||
const toggleStatus = useCallback(
|
||||
|
@ -68,7 +68,7 @@ export const useDocumentDetail = (documentId, options = null) => {
|
|||
mutate();
|
||||
return ret;
|
||||
},
|
||||
[mutate]
|
||||
[mutate, documentId]
|
||||
);
|
||||
|
||||
return { data, loading, error, update, toggleStatus };
|
||||
|
@ -118,7 +118,7 @@ export const useDocumentStar = (documentId) => {
|
|||
targetId: documentId,
|
||||
});
|
||||
mutate();
|
||||
}, [mutate]);
|
||||
}, [mutate, documentId]);
|
||||
|
||||
return { data, error, toggleStar };
|
||||
};
|
||||
|
@ -198,7 +198,7 @@ export const useCollaborationDocument = (documentId) => {
|
|||
mutate();
|
||||
return ret;
|
||||
},
|
||||
[mutate]
|
||||
[mutate, documentId]
|
||||
);
|
||||
|
||||
const updateUser = useCallback(
|
||||
|
@ -210,7 +210,7 @@ export const useCollaborationDocument = (documentId) => {
|
|||
mutate();
|
||||
return ret;
|
||||
},
|
||||
[mutate]
|
||||
[mutate, documentId]
|
||||
);
|
||||
|
||||
const deleteUser = useCallback(
|
||||
|
@ -222,7 +222,7 @@ export const useCollaborationDocument = (documentId) => {
|
|||
mutate();
|
||||
return ret;
|
||||
},
|
||||
[mutate]
|
||||
[mutate, documentId]
|
||||
);
|
||||
|
||||
return { users: data, loading, error, addUser, updateUser, deleteUser };
|
||||
|
|
|
@ -62,12 +62,12 @@ export const useTemplate = (templateId) => {
|
|||
mutate();
|
||||
return ret as unknown as ITemplate;
|
||||
},
|
||||
[mutate]
|
||||
[mutate, templateId]
|
||||
);
|
||||
|
||||
const deleteTemplate = useCallback(async () => {
|
||||
await HttpClient.post(`/template/delete/${templateId}`);
|
||||
}, []);
|
||||
}, [templateId]);
|
||||
|
||||
return {
|
||||
data,
|
||||
|
|
|
@ -104,7 +104,7 @@ export const useWikiTocs = (wikiId) => {
|
|||
mutate();
|
||||
return res;
|
||||
},
|
||||
[mutate]
|
||||
[mutate, wikiId]
|
||||
);
|
||||
|
||||
return { data, loading, error, refresh: mutate, update };
|
||||
|
@ -143,7 +143,7 @@ export const useWikiDetail = (wikiId) => {
|
|||
mutate();
|
||||
return res;
|
||||
},
|
||||
[mutate]
|
||||
[mutate, wikiId]
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -157,7 +157,7 @@ export const useWikiDetail = (wikiId) => {
|
|||
mutate();
|
||||
return res;
|
||||
},
|
||||
[mutate]
|
||||
[mutate, wikiId]
|
||||
);
|
||||
|
||||
return { data, loading, error, update, toggleStatus };
|
||||
|
@ -178,7 +178,7 @@ export const useWikiUsers = (wikiId) => {
|
|||
mutate();
|
||||
return ret;
|
||||
},
|
||||
[mutate]
|
||||
[mutate, wikiId]
|
||||
);
|
||||
|
||||
const updateUser = useCallback(
|
||||
|
@ -187,7 +187,7 @@ export const useWikiUsers = (wikiId) => {
|
|||
mutate();
|
||||
return ret;
|
||||
},
|
||||
[mutate]
|
||||
[mutate, wikiId]
|
||||
);
|
||||
|
||||
const deleteUser = useCallback(
|
||||
|
@ -196,7 +196,7 @@ export const useWikiUsers = (wikiId) => {
|
|||
mutate();
|
||||
return ret;
|
||||
},
|
||||
[mutate]
|
||||
[mutate, wikiId]
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -229,7 +229,7 @@ export const useWikiStar = (wikiId) => {
|
|||
targetId: wikiId,
|
||||
});
|
||||
mutate();
|
||||
}, [mutate]);
|
||||
}, [mutate, wikiId]);
|
||||
|
||||
return { data, error, toggleStar };
|
||||
};
|
||||
|
|
|
@ -2,9 +2,10 @@ import type { AppProps } from 'next/app';
|
|||
import Head from 'next/head';
|
||||
import React from 'react';
|
||||
import { useTheme } from 'hooks/use-theme';
|
||||
import 'tiptap/fix-match-nodes';
|
||||
import 'viewerjs/dist/viewer.css';
|
||||
import 'styles/globals.scss';
|
||||
import 'tiptap/styles/index.scss';
|
||||
import 'tiptap/core/styles/index.scss';
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
useTheme();
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { SingleColumnLayout } from 'layouts/single-column';
|
||||
import { ICollaborationEditorProps, CollaborationEditor, ICollaborationRefProps } from 'tiptap/editor';
|
||||
import { useUser } from 'data/user';
|
||||
|
||||
const Page = () => {
|
||||
const $container = useRef<HTMLDivElement>();
|
||||
const { user } = useUser();
|
||||
|
||||
const { query } = useRouter();
|
||||
const { type, id } = query as { type: ICollaborationEditorProps['type']; id: string };
|
||||
|
||||
return (
|
||||
<SingleColumnLayout>
|
||||
<div className="container" style={{ height: 400 }} ref={$container}>
|
||||
{type && id ? (
|
||||
<>
|
||||
{user && <CollaborationEditor menubar editable user={user} id={id} type={type} />}
|
||||
<br />
|
||||
{user && <CollaborationEditor menubar editable user={user} id={id} type={type} />}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</SingleColumnLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
|
@ -87,40 +87,3 @@ select {
|
|||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track-piece {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner,
|
||||
::-webkit-resizer {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
*:hover {
|
||||
&::-webkit-scrollbar-thumb &::-webkit-scrollbar-corner,
|
||||
&::-webkit-resizer {
|
||||
background-color: var(--scrollbar-bg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Node, mergeAttributes } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||
import { AttachmentWrapper } from 'tiptap/wrappers/attachment';
|
||||
import { AttachmentWrapper } from 'tiptap/core/wrappers/attachment';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
|
@ -1,7 +1,6 @@
|
|||
import { Blockquote as BuiltInBlockquote } from '@tiptap/extension-blockquote';
|
||||
import { wrappingInputRule } from '@tiptap/core';
|
||||
import { getParents } from 'tiptap/prose-utils';
|
||||
import { getMarkdownSource } from 'tiptap/markdown/markdown-to-prosemirror';
|
||||
import { getParents, getMarkdownSource } from 'tiptap/prose-utils';
|
||||
|
||||
export const Blockquote = BuiltInBlockquote.extend({
|
||||
addAttributes() {
|
|
@ -1,5 +1,5 @@
|
|||
import { BulletList as BuiltInBulletList } from '@tiptap/extension-bullet-list';
|
||||
import { getMarkdownSource } from 'tiptap/markdown/markdown-to-prosemirror';
|
||||
import { getMarkdownSource } from 'tiptap/prose-utils';
|
||||
|
||||
export const BulletList = BuiltInBulletList.extend({
|
||||
addAttributes() {
|
|
@ -1,6 +1,6 @@
|
|||
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { CalloutWrapper } from 'tiptap/wrappers/callout';
|
||||
import { CalloutWrapper } from 'tiptap/core/wrappers/callout';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
|
@ -3,7 +3,7 @@ import { Plugin, PluginKey, TextSelection } from 'prosemirror-state';
|
|||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { lowlight } from 'lowlight/lib/all';
|
||||
import { LowlightPlugin } from 'tiptap/prose-utils';
|
||||
import { CodeBlockWrapper } from 'tiptap/wrappers/code-block';
|
||||
import { CodeBlockWrapper } from 'tiptap/core/wrappers/code-block';
|
||||
|
||||
export interface CodeBlockOptions {
|
||||
/**
|
|
@ -1,5 +1,5 @@
|
|||
import BuiltInCode from '@tiptap/extension-code';
|
||||
import { EXTENSION_PRIORITY_LOWER } from 'tiptap/constants';
|
||||
import { EXTENSION_PRIORITY_LOWER } from 'tiptap/core/constants';
|
||||
|
||||
export const Code = BuiltInCode.extend({
|
||||
excludes: null,
|
|
@ -1,6 +1,6 @@
|
|||
import { Node, mergeAttributes } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { CountdownWrapper } from 'tiptap/wrappers/countdown';
|
||||
import { CountdownWrapper } from 'tiptap/core/wrappers/countdown';
|
||||
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||
|
||||
declare module '@tiptap/core' {
|
|
@ -1,6 +1,6 @@
|
|||
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { DocumentChildrenWrapper } from 'tiptap/wrappers/document-children';
|
||||
import { DocumentChildrenWrapper } from 'tiptap/core/wrappers/document-children';
|
||||
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||
|
||||
declare module '@tiptap/core' {
|
|
@ -1,6 +1,6 @@
|
|||
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { DocumentReferenceWrapper } from 'tiptap/wrappers/document-reference';
|
||||
import { DocumentReferenceWrapper } from 'tiptap/core/wrappers/document-reference';
|
||||
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||
|
||||
declare module '@tiptap/core' {
|
|
@ -3,9 +3,9 @@ import { ReactRenderer } from '@tiptap/react';
|
|||
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||
import Suggestion from '@tiptap/suggestion';
|
||||
import tippy from 'tippy.js';
|
||||
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/constants';
|
||||
import { EmojiList } from 'tiptap/wrappers/emoji-list';
|
||||
import { emojiSearch, emojisToName } from 'tiptap/wrappers/emoji-list/emojis';
|
||||
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
||||
import { EmojiList } from 'tiptap/core/wrappers/emoji-list';
|
||||
import { emojiSearch, emojisToName } from 'tiptap/core/wrappers/emoji-list/emojis';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
|
@ -0,0 +1,19 @@
|
|||
import { Extension } from '@tiptap/core';
|
||||
import { EventEmitter as Em } from 'helpers/event-emitter';
|
||||
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
||||
|
||||
const event = new Em();
|
||||
|
||||
/**
|
||||
* 添加事件能力
|
||||
*/
|
||||
export const EventEmitter = Extension.create({
|
||||
name: 'eventEmitter',
|
||||
priority: EXTENSION_PRIORITY_HIGHEST,
|
||||
addOptions() {
|
||||
return { eventEmitter: event };
|
||||
},
|
||||
addStorage() {
|
||||
return this.options;
|
||||
},
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import { Mark, mergeAttributes, markInputRule } from '@tiptap/core';
|
||||
import { PARSE_HTML_PRIORITY_LOWEST } from 'tiptap/constants';
|
||||
import { PARSE_HTML_PRIORITY_LOWEST } from 'tiptap/core/constants';
|
||||
import { markInputRegex, extractMarkAttributesFromMatch } from 'tiptap/prose-utils';
|
||||
|
||||
export const marks = [{ name: 'underline', tag: 'u' }];
|
|
@ -1,6 +1,6 @@
|
|||
import { Node, mergeAttributes } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { IframeWrapper } from 'tiptap/wrappers/iframe';
|
||||
import { IframeWrapper } from 'tiptap/core/wrappers/iframe';
|
||||
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||
|
||||
declare module '@tiptap/core' {
|
|
@ -1,6 +1,6 @@
|
|||
import { Image as BuiltInImage } from '@tiptap/extension-image';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { ImageWrapper } from 'tiptap/wrappers/image';
|
||||
import { ImageWrapper } from 'tiptap/core/wrappers/image';
|
||||
|
||||
const resolveImageEl = (element) => (element.nodeName === 'IMG' ? element : element.querySelector('img'));
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { KatexWrapper } from 'tiptap/wrappers/katex';
|
||||
import { KatexWrapper } from 'tiptap/core/wrappers/katex';
|
||||
|
||||
type IKatexAttrs = {
|
||||
text?: string;
|
|
@ -1,6 +1,6 @@
|
|||
import { Node } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { LoadingWrapper } from 'tiptap/wrappers/loading';
|
||||
import { LoadingWrapper } from 'tiptap/core/wrappers/loading';
|
||||
|
||||
export const Loading = Node.create({
|
||||
name: 'loading',
|
|
@ -3,7 +3,7 @@ import { ReactRenderer } from '@tiptap/react';
|
|||
import tippy from 'tippy.js';
|
||||
import { getUsers } from 'services/user';
|
||||
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||
import { MentionList } from 'tiptap/wrappers/mention-list';
|
||||
import { MentionList } from 'tiptap/core/wrappers/mention-list';
|
||||
|
||||
const suggestion = {
|
||||
items: async ({ query }) => {
|
|
@ -1,7 +1,7 @@
|
|||
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||
import { MindWrapper } from 'tiptap/wrappers/mind';
|
||||
import { MindWrapper } from 'tiptap/core/wrappers/mind';
|
||||
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||
|
||||
const DEFAULT_MIND_DATA = {
|
|
@ -1,5 +1,5 @@
|
|||
import { OrderedList as BuiltInOrderedList } from '@tiptap/extension-ordered-list';
|
||||
import { getMarkdownSource } from 'tiptap/markdown/markdown-to-prosemirror';
|
||||
import { getMarkdownSource } from 'tiptap/prose-utils';
|
||||
|
||||
export const OrderedList = BuiltInOrderedList.extend({
|
||||
addAttributes() {
|
|
@ -1,16 +1,30 @@
|
|||
import { Extension } from '@tiptap/core';
|
||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/constants';
|
||||
import { Schema, Fragment } from 'prosemirror-model';
|
||||
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
||||
import { handleFileEvent, isInCode, LANGUAGES, isTitleNode } from 'tiptap/prose-utils';
|
||||
import {
|
||||
isMarkdown,
|
||||
normalizePastedMarkdown,
|
||||
markdownToProsemirror,
|
||||
prosemirrorToMarkdown,
|
||||
} from 'tiptap/markdown/markdown-to-prosemirror';
|
||||
import { copyNode } from 'tiptap/prose-utils';
|
||||
import { copyNode, isMarkdown, normalizeMarkdown } from 'tiptap/prose-utils';
|
||||
import { safeJSONParse } from 'helpers/json';
|
||||
|
||||
interface IPasteOptions {
|
||||
/**
|
||||
*
|
||||
* 将 markdown 转换为 html
|
||||
*/
|
||||
markdownToHTML: (arg: string) => string;
|
||||
|
||||
/**
|
||||
* 将 markdown 转换为 prosemirror 节点
|
||||
* FIXME: prosemirror 节点的类型是什么?
|
||||
*/
|
||||
markdownToProsemirror: (arg: { schema: Schema; content: string; hasTitle: boolean }) => unknown;
|
||||
|
||||
/**
|
||||
* 将 prosemirror 转换为 markdown
|
||||
*/
|
||||
prosemirrorToMarkdown: (arg: { content: Fragment }) => string;
|
||||
}
|
||||
|
||||
const isPureText = (content): boolean => {
|
||||
if (!content) return false;
|
||||
|
||||
|
@ -27,11 +41,25 @@ const isPureText = (content): boolean => {
|
|||
return content['type'] === 'text';
|
||||
};
|
||||
|
||||
export const Paste = Extension.create({
|
||||
export const Paste = Extension.create<IPasteOptions>({
|
||||
name: 'paste',
|
||||
priority: EXTENSION_PRIORITY_HIGHEST,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
markdownToHTML: (arg) => arg,
|
||||
markdownToProsemirror: (arg) => arg.content,
|
||||
prosemirrorToMarkdown: (arg) => String(arg.content),
|
||||
};
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return this.options;
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const { editor } = this;
|
||||
const extensionThis = this;
|
||||
const { editor } = extensionThis;
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
|
@ -41,9 +69,8 @@ export const Paste = Extension.create({
|
|||
if (view.props.editable && !view.props.editable(view.state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!event.clipboardData) return false;
|
||||
|
||||
// 文件
|
||||
const files = Array.from(event.clipboardData.files);
|
||||
if (files.length) {
|
||||
event.preventDefault();
|
||||
|
@ -53,12 +80,14 @@ export const Paste = Extension.create({
|
|||
return true;
|
||||
}
|
||||
|
||||
const { markdownToProsemirror } = extensionThis.options;
|
||||
const text = event.clipboardData.getData('text/plain');
|
||||
const html = event.clipboardData.getData('text/html');
|
||||
const vscode = event.clipboardData.getData('vscode-editor-data');
|
||||
const node = event.clipboardData.getData('text/node');
|
||||
const markdownText = event.clipboardData.getData('text/markdown');
|
||||
|
||||
// 直接复制节点
|
||||
if (node) {
|
||||
const doc = safeJSONParse(node);
|
||||
const tr = view.state.tr;
|
||||
|
@ -98,7 +127,7 @@ export const Paste = Extension.create({
|
|||
const schema = view.props.state.schema;
|
||||
const doc = markdownToProsemirror({
|
||||
schema,
|
||||
content: normalizePastedMarkdown(markdownText || text),
|
||||
content: normalizeMarkdown(markdownText || text),
|
||||
hasTitle,
|
||||
});
|
||||
let tr = view.state.tr;
|
||||
|
@ -111,13 +140,11 @@ export const Paste = Extension.create({
|
|||
view.dispatch(tr.scrollIntoView());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (text.length !== 0) {
|
||||
event.preventDefault();
|
||||
view.dispatch(view.state.tr.insertText(text));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
handleDrop: (view, event: any) => {
|
||||
|
@ -171,7 +198,7 @@ export const Paste = Extension.create({
|
|||
if (!doc) {
|
||||
return '';
|
||||
}
|
||||
const content = prosemirrorToMarkdown({
|
||||
const content = extensionThis.options.prosemirrorToMarkdown({
|
||||
content: doc,
|
||||
});
|
||||
return content;
|
|
@ -3,9 +3,9 @@ import { Node } from '@tiptap/core';
|
|||
import { ReactRenderer } from '@tiptap/react';
|
||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||
import Suggestion from '@tiptap/suggestion';
|
||||
import { MenuList } from 'tiptap/wrappers/menu-list';
|
||||
import { QUICK_INSERT_ITEMS } from 'tiptap/menus/quick-insert';
|
||||
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/constants';
|
||||
import { MenuList } from 'tiptap/core/wrappers/menu-list';
|
||||
import { QUICK_INSERT_ITEMS } from 'tiptap/editor/menus/quick-insert';
|
||||
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
||||
|
||||
export const QuickInsertPluginKey = new PluginKey('quickInsert');
|
||||
|
|
@ -2,7 +2,7 @@ import { Extension } from '@tiptap/core';
|
|||
import { Plugin, PluginKey, NodeSelection, TextSelection, Selection, AllSelection } from 'prosemirror-state';
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||
import { getCurrentNode, isInCodeBlock, isInCallout } from 'tiptap/prose-utils';
|
||||
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/constants';
|
||||
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
||||
|
||||
export const selectionPluginKey = new PluginKey('selection');
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Node, mergeAttributes } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { StatusWrapper } from 'tiptap/wrappers/status';
|
||||
import { StatusWrapper } from 'tiptap/core/wrappers/status';
|
||||
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||
|
||||
type IStatusAttrs = {
|
|
@ -2,7 +2,7 @@ import { wrappingInputRule } from '@tiptap/core';
|
|||
import { TaskItem as BuiltInTaskItem } from '@tiptap/extension-task-item';
|
||||
import { Plugin } from 'prosemirror-state';
|
||||
import { findParentNodeClosestToPos } from 'prosemirror-utils';
|
||||
import { PARSE_HTML_PRIORITY_HIGHEST } from 'tiptap/constants';
|
||||
import { PARSE_HTML_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
||||
|
||||
const CustomTaskItem = BuiltInTaskItem.extend({
|
||||
parseHTML() {
|
|
@ -1,5 +1,5 @@
|
|||
import { TaskList as BuiltInTaskList } from '@tiptap/extension-task-list';
|
||||
import { PARSE_HTML_PRIORITY_HIGHEST } from 'tiptap/constants';
|
||||
import { PARSE_HTML_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
||||
|
||||
export const TaskList = BuiltInTaskList.extend({
|
||||
parseHTML() {
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue