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",
|
"react-split-pane": "^0.1.92",
|
||||||
"scroll-into-view-if-needed": "^2.2.29",
|
"scroll-into-view-if-needed": "^2.2.29",
|
||||||
"swr": "^1.2.0",
|
"swr": "^1.2.0",
|
||||||
|
"tilg": "^0.1.1",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"toggle-selection": "^1.0.6",
|
"toggle-selection": "^1.0.6",
|
||||||
"viewerjs": "^1.10.4",
|
"viewerjs": "^1.10.4",
|
||||||
|
|
|
@ -69,8 +69,9 @@ export const DataRender: React.FC<IProps> = ({
|
||||||
return (
|
return (
|
||||||
<LoadingWrap
|
<LoadingWrap
|
||||||
loading={loading}
|
loading={loading}
|
||||||
loadingContent={runRender(loadingContent)}
|
runRender={runRender}
|
||||||
normalContent={loading ? null : runRender(normalContent)}
|
loadingContent={loadingContent}
|
||||||
|
normalContent={loading ? null : normalContent}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
|
|
||||||
interface IProps {
|
// interface IProps {
|
||||||
loading: boolean;
|
// loading: boolean;
|
||||||
delay?: number;
|
// delay?: number;
|
||||||
loadingContent: React.ReactElement;
|
// runRender
|
||||||
normalContent: React.ReactElement;
|
// 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 timer = useRef<ReturnType<typeof setTimeout>>(null);
|
||||||
const [showLoading, toggleShowLoading] = useToggle(false);
|
const [showLoading, toggleShowLoading] = useToggle(false);
|
||||||
|
|
||||||
|
@ -31,8 +32,8 @@ export const LoadingWrap: React.FC<IProps> = ({ loading, delay = 200, loadingCon
|
||||||
}, [delay, loading, toggleShowLoading]);
|
}, [delay, loading, toggleShowLoading]);
|
||||||
|
|
||||||
if (loading) {
|
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 React, { useRef, useState } from 'react';
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
|
||||||
import { Avatar, Button, Space, Typography, Banner, Pagination } from '@douyinfe/semi-ui';
|
import { Avatar, Button, Space, Typography, Banner, Pagination } from '@douyinfe/semi-ui';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
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 { DataRender } from 'components/data-render';
|
||||||
import { useUser } from 'data/user';
|
import { useUser } from 'data/user';
|
||||||
import { useComments } from 'data/comment';
|
import { useComments } from 'data/comment';
|
||||||
|
@ -34,7 +33,7 @@ export const CommentEditor: React.FC<IProps> = ({ documentId }) => {
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
editable: true,
|
editable: true,
|
||||||
extensions: [...CommentKit, Document, History],
|
extensions: CommentKit,
|
||||||
});
|
});
|
||||||
|
|
||||||
const openEditor = () => {
|
const openEditor = () => {
|
||||||
|
|
|
@ -1,33 +1,12 @@
|
||||||
import Router from 'next/router';
|
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 cls from 'classnames';
|
||||||
import deepEqual from 'deep-equal';
|
|
||||||
import { BackTop, Toast, Spin, Typography } from '@douyinfe/semi-ui';
|
|
||||||
import { ILoginUser, IAuthority } from '@think/domains';
|
import { ILoginUser, IAuthority } from '@think/domains';
|
||||||
import { SecureDocumentIllustration } from 'illustrations/secure-document';
|
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
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 { findMentions } from 'tiptap/prose-utils';
|
||||||
import { useCollaborationDocument } from 'data/document';
|
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 { event, triggerChangeDocumentTitle, triggerJoinUser, USE_DOCUMENT_VERSION } from 'event';
|
||||||
|
import { CollaborationEditor, ICollaborationRefProps } from 'tiptap/editor';
|
||||||
import { DocumentUserSetting } from './users';
|
import { DocumentUserSetting } from './users';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
@ -39,93 +18,31 @@ interface IProps {
|
||||||
style: React.CSSProperties;
|
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 $hasShowUserSettingModal = useRef(false);
|
||||||
|
const $editor = useRef<ICollaborationRefProps>();
|
||||||
const { users, addUser, updateUser } = useCollaborationDocument(documentId);
|
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 [mentionUsersSettingVisible, toggleMentionUsersSettingVisible] = useToggle(false);
|
||||||
const [mentionUsers, setMentionUsers] = useState([]);
|
const [mentionUsers, setMentionUsers] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authority || !authority.editable) return;
|
const handler = (data) => {
|
||||||
|
const editor = $editor.current && $editor.current.getEditor();
|
||||||
const indexdbProvider = getIndexdbProvider(documentId, provider.document);
|
if (!editor) return;
|
||||||
|
editor.commands.setContent(data);
|
||||||
indexdbProvider.on('synced', () => {
|
|
||||||
setStatus('loadCacheSuccess');
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
destoryIndexdbProvider(documentId);
|
|
||||||
};
|
};
|
||||||
}, [documentId, provider, authority]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!editor) return;
|
|
||||||
const handler = (data) => editor.commands.setContent(data);
|
|
||||||
event.on(USE_DOCUMENT_VERSION, handler);
|
event.on(USE_DOCUMENT_VERSION, handler);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
event.off(USE_DOCUMENT_VERSION, handler);
|
event.off(USE_DOCUMENT_VERSION, handler);
|
||||||
};
|
};
|
||||||
}, [editor]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) return;
|
|
||||||
|
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
|
const editor = $editor.current && $editor.current.getEditor();
|
||||||
|
if (!editor) return;
|
||||||
|
|
||||||
// 已经拦截过一次,不再拦截
|
// 已经拦截过一次,不再拦截
|
||||||
if ($hasShowUserSettingModal.current) return;
|
if ($hasShowUserSettingModal.current) return;
|
||||||
|
|
||||||
|
@ -169,98 +86,28 @@ export const _Editor: React.FC<IProps> = ({ user: currentUser, documentId, autho
|
||||||
Router.events.off('routeChangeStart', handler);
|
Router.events.off('routeChangeStart', handler);
|
||||||
window.removeEventListener('unload', handler);
|
window.removeEventListener('unload', handler);
|
||||||
};
|
};
|
||||||
}, [editor, users, currentUser, toggleMentionUsersSettingVisible]);
|
}, [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);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataRender
|
<div className={cls(styles.editorWrap, className)} style={style}>
|
||||||
loading={loading}
|
<CollaborationEditor
|
||||||
loadingContent={
|
ref={$editor}
|
||||||
<div style={{ margin: '10vh auto' }}>
|
menubar
|
||||||
<Spin tip="正在为您加载编辑器中...">
|
editable={authority && authority.editable}
|
||||||
{/* FIXME: semi-design 的问题,不加 div,文字会换行! */}
|
user={currentUser}
|
||||||
<div></div>
|
id={documentId}
|
||||||
</Spin>
|
type="document"
|
||||||
</div>
|
onTitleUpdate={triggerChangeDocumentTitle}
|
||||||
}
|
onAwarenessUpdate={triggerJoinUser}
|
||||||
error={error}
|
/>
|
||||||
errorContent={(error) => (
|
<DocumentUserSetting
|
||||||
<div
|
visible={mentionUsersSettingVisible}
|
||||||
style={{
|
toggleVisible={toggleMentionUsersSettingVisible}
|
||||||
margin: '10vh',
|
mentionUsers={mentionUsers}
|
||||||
display: 'flex',
|
users={users}
|
||||||
justifyContent: 'center',
|
addUser={addUser}
|
||||||
flexDirection: 'column',
|
updateUser={updateUser}
|
||||||
alignItems: 'center',
|
/>
|
||||||
}}
|
</div>
|
||||||
>
|
|
||||||
<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="我们已与您断开连接,您可以继续编辑文档。一旦重新连接,我们会自动重新提交数据。"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{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}
|
|
||||||
mentionUsers={mentionUsers}
|
|
||||||
users={users}
|
|
||||||
addUser={addUser}
|
|
||||||
updateUser={updateUser}
|
|
||||||
/>
|
|
||||||
</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 {
|
.wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -30,51 +31,31 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
> header {
|
> div {
|
||||||
position: relative;
|
> main {
|
||||||
z-index: 110;
|
padding: 24px 24px 96px;
|
||||||
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 {
|
&.isStandardWidth {
|
||||||
flex: 1;
|
> div {
|
||||||
height: calc(100% - 50px);
|
> main {
|
||||||
overflow: auto;
|
> div:first-of-type {
|
||||||
|
width: 96%;
|
||||||
.contentWrap {
|
max-width: 750px;
|
||||||
padding: 24px 24px 96px;
|
margin: 0 auto;
|
||||||
|
}
|
||||||
&.isStandardWidth {
|
|
||||||
width: 96%;
|
|
||||||
max-width: 750px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.isFullWidth {
|
&.isFullWidth {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
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} />
|
<Skeleton active placeholder={<Skeleton.Title style={{ width: 80 }} />} loading={true} />
|
||||||
}
|
}
|
||||||
normalContent={() => (
|
normalContent={() => (
|
||||||
<Text ellipsis={{ showTooltip: true }} style={{ width: ~~(windowWith / 4) }}>
|
<Text
|
||||||
|
ellipsis={{
|
||||||
|
showTooltip: { opts: { content: title, style: { wordBreak: 'break-all' } } },
|
||||||
|
}}
|
||||||
|
style={{ width: ~~(windowWith / 4) }}
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</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 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 cls from 'classnames';
|
||||||
import { Layout, Nav, Space, Button, Typography, Skeleton, Tooltip, Popover, BackTop, Spin } from '@douyinfe/semi-ui';
|
import {
|
||||||
import { IconEdit, IconArticle } from '@douyinfe/semi-icons';
|
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 { Seo } from 'components/seo';
|
||||||
import { DataRender } from 'components/data-render';
|
import { DataRender } from 'components/data-render';
|
||||||
import { DocumentShare } from 'components/document/share';
|
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 { useWindowSize } from 'hooks/use-window-size';
|
||||||
import { useUser } from 'data/user';
|
import { useUser } from 'data/user';
|
||||||
import { useDocumentDetail } from 'data/document';
|
import { useDocumentDetail } from 'data/document';
|
||||||
import { Editor } from './editor';
|
import { triggerJoinUser } from 'event';
|
||||||
|
import { CollaborationEditor } from 'tiptap/editor';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
const { Header } = Layout;
|
const { Header } = Layout;
|
||||||
const { Text } = Typography;
|
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 {
|
interface IProps {
|
||||||
documentId: string;
|
documentId: string;
|
||||||
|
@ -36,6 +63,51 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
||||||
const { data: documentAndAuth, loading: docAuthLoading, error: docAuthError } = useDocumentDetail(documentId);
|
const { data: documentAndAuth, loading: docAuthLoading, error: docAuthError } = useDocumentDetail(documentId);
|
||||||
const { document, authority } = documentAndAuth || {};
|
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(() => {
|
const gotoEdit = useCallback(() => {
|
||||||
Router.push(`/wiki/${document.wikiId}/document/${document.id}/edit`);
|
Router.push(`/wiki/${document.wikiId}/document/${document.id}/edit`);
|
||||||
}, [document]);
|
}, [document]);
|
||||||
|
@ -54,7 +126,13 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
||||||
error={docAuthError}
|
error={docAuthError}
|
||||||
loadingContent={<Skeleton active placeholder={<Skeleton.Title style={{ width: 80 }} />} loading={true} />}
|
loadingContent={<Skeleton active placeholder={<Skeleton.Title style={{ width: 80 }} />} loading={true} />}
|
||||||
normalContent={() => (
|
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}
|
{document.title}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
@ -103,30 +181,22 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
||||||
<>
|
<>
|
||||||
<Seo title={document.title} />
|
<Seo title={document.title} />
|
||||||
{user && (
|
{user && (
|
||||||
<Editor key={document.id} user={user} documentId={document.id} document={document}>
|
<CollaborationEditor
|
||||||
<div className={styles.commentWrap}>
|
editable={false}
|
||||||
<CommentEditor documentId={document.id} />
|
user={user}
|
||||||
</div>
|
id={documentId}
|
||||||
</Editor>
|
type="document"
|
||||||
|
renderInEditorPortal={renderAuthor}
|
||||||
|
onAwarenessUpdate={triggerJoinUser}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{user && (
|
||||||
|
<div className={styles.commentWrap}>
|
||||||
|
<CommentEditor documentId={document.id} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{authority && authority.editable && container && (
|
{authority && authority.editable && container && (
|
||||||
<BackTop
|
<BackTop style={EditBtnStyle} onClick={gotoEdit} target={() => container} visibilityHeight={200}>
|
||||||
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}
|
|
||||||
>
|
|
||||||
<IconEdit />
|
<IconEdit />
|
||||||
</BackTop>
|
</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 { ImageViewer } from 'components/image-viewer';
|
||||||
import { useDocumentStyle } from 'hooks/use-document-style';
|
import { useDocumentStyle } from 'hooks/use-document-style';
|
||||||
import { usePublicDocument } from 'data/document';
|
import { usePublicDocument } from 'data/document';
|
||||||
import { DocumentSkeleton } from 'tiptap';
|
import { DocumentSkeleton } from 'tiptap/components/skeleton';
|
||||||
import { DocumentContent } from './content';
|
import { CollaborationEditor } from 'tiptap/editor';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
const { Header, Content } = Layout;
|
const { Header, Content } = Layout;
|
||||||
|
@ -121,26 +121,21 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
|
||||||
}}
|
}}
|
||||||
loadingContent={
|
loadingContent={
|
||||||
<div className={cls(styles.editorWrap, editorWrapClassNames)} style={{ fontSize }}>
|
<div className={cls(styles.editorWrap, editorWrapClassNames)} style={{ fontSize }}>
|
||||||
<DocumentSkeleton />
|
1<DocumentSkeleton />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
normalContent={() => {
|
normalContent={() => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
|
id="js-share-document-editor-container"
|
||||||
|
className={cls(styles.editorWrap, editorWrapClassNames)}
|
||||||
|
style={{ fontSize }}
|
||||||
|
>
|
||||||
<Seo title={data.title} />
|
<Seo title={data.title} />
|
||||||
<div
|
<CollaborationEditor menubar={false} editable={false} user={null} id={documentId} type="document" />
|
||||||
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>
|
|
||||||
<ImageViewer containerSelector="#js-share-document-editor-container" />
|
<ImageViewer containerSelector="#js-share-document-editor-container" />
|
||||||
<BackTop target={() => document.querySelector('#js-share-document-editor-container').parentNode} />
|
<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 { IconChevronLeft } from '@douyinfe/semi-icons';
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
import { useEditor, EditorContent } from '@tiptap/react';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import { BaseKit, DocumentWithTitle } from 'tiptap';
|
import { CollaborationKit } from 'tiptap/editor';
|
||||||
import { safeJSONParse } from 'helpers/json';
|
import { safeJSONParse } from 'helpers/json';
|
||||||
import { DataRender } from 'components/data-render';
|
import { DataRender } from 'components/data-render';
|
||||||
import { LocaleTime } from 'components/locale-time';
|
import { LocaleTime } from 'components/locale-time';
|
||||||
|
@ -25,7 +25,7 @@ export const DocumentVersion: React.FC<IProps> = ({ documentId, onSelect }) => {
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
editable: false,
|
editable: false,
|
||||||
extensions: [...BaseKit, DocumentWithTitle],
|
extensions: CollaborationKit,
|
||||||
content: { type: 'doc', content: [] },
|
content: { type: 'doc', content: [] },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,35 +1,15 @@
|
||||||
import React, { useMemo, useCallback, useState, useEffect } from 'react';
|
import React, { useMemo, useCallback, useState, useEffect } from 'react';
|
||||||
import Router from 'next/router';
|
import Router from 'next/router';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import {
|
import { Button, Nav, Space, Typography, Tooltip, Switch, Popover, Popconfirm } from '@douyinfe/semi-ui';
|
||||||
Button,
|
|
||||||
Nav,
|
|
||||||
Space,
|
|
||||||
Typography,
|
|
||||||
Tooltip,
|
|
||||||
Switch,
|
|
||||||
Popover,
|
|
||||||
Popconfirm,
|
|
||||||
BackTop,
|
|
||||||
Toast,
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import { IconChevronLeft, IconArticle } from '@douyinfe/semi-icons';
|
import { IconChevronLeft, IconArticle } from '@douyinfe/semi-icons';
|
||||||
import { ILoginUser, ITemplate } from '@think/domains';
|
import { ILoginUser, ITemplate } from '@think/domains';
|
||||||
import { Theme } from 'components/theme';
|
import { Theme } from 'components/theme';
|
||||||
import {
|
|
||||||
useEditor,
|
|
||||||
EditorContent,
|
|
||||||
BaseKit,
|
|
||||||
DocumentWithTitle,
|
|
||||||
getCollaborationExtension,
|
|
||||||
getProvider,
|
|
||||||
MenuBar,
|
|
||||||
} from 'tiptap';
|
|
||||||
import { User } from 'components/user';
|
import { User } from 'components/user';
|
||||||
import { DocumentStyle } from 'components/document/style';
|
import { DocumentStyle } from 'components/document/style';
|
||||||
import { LogoName } from 'components/logo';
|
|
||||||
import { useDocumentStyle } from 'hooks/use-document-style';
|
import { useDocumentStyle } from 'hooks/use-document-style';
|
||||||
import { useWindowSize } from 'hooks/use-window-size';
|
import { useWindowSize } from 'hooks/use-window-size';
|
||||||
|
import { CollaborationEditor } from 'tiptap/editor';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
@ -44,30 +24,6 @@ interface IProps {
|
||||||
export const Editor: React.FC<IProps> = ({ user, data, updateTemplate, deleteTemplate }) => {
|
export const Editor: React.FC<IProps> = ({ user, data, updateTemplate, deleteTemplate }) => {
|
||||||
const { width: windowWidth } = useWindowSize();
|
const { width: windowWidth } = useWindowSize();
|
||||||
const [title, setTitle] = useState(data.title);
|
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 [isPublic, setPublic] = useState(false);
|
||||||
const { width, fontSize } = useDocumentStyle();
|
const { width, fontSize } = useDocumentStyle();
|
||||||
const editorWrapClassNames = useMemo(() => {
|
const editorWrapClassNames = useMemo(() => {
|
||||||
|
@ -89,22 +45,6 @@ export const Editor: React.FC<IProps> = ({ user, data, updateTemplate, deleteTem
|
||||||
setPublic(data.isPublic);
|
setPublic(data.isPublic);
|
||||||
}, [data]);
|
}, [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 (
|
return (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<header>
|
<header>
|
||||||
|
@ -140,17 +80,9 @@ export const Editor: React.FC<IProps> = ({ user, data, updateTemplate, deleteTem
|
||||||
</header>
|
</header>
|
||||||
<main className={styles.contentWrap}>
|
<main className={styles.contentWrap}>
|
||||||
<div className={styles.editorWrap}>
|
<div className={styles.editorWrap}>
|
||||||
<header className={editorWrapClassNames}>
|
<div className={cls(styles.contentWrap, editorWrapClassNames)} style={{ fontSize }}>
|
||||||
<div>
|
<CollaborationEditor menubar editable user={user} id={data.id} type="template" onTitleUpdate={setTitle} />
|
||||||
<MenuBar editor={editor} />
|
</div>
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main id="js-template-editor-container">
|
|
||||||
<div className={cls(styles.contentWrap, editorWrapClassNames)} style={{ fontSize }}>
|
|
||||||
<EditorContent editor={editor} />
|
|
||||||
</div>
|
|
||||||
<BackTop target={() => document.querySelector('#js-template-editor-container')} />
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* stylelint-disable no-descending-specificity */
|
||||||
.wrap {
|
.wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -30,55 +31,33 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
> header {
|
> div {
|
||||||
position: relative;
|
height: 100%;
|
||||||
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:first-of-type > main {
|
||||||
|
padding: 24px 24px 96px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.isStandardWidth {
|
||||||
> div {
|
> div {
|
||||||
display: inline-flex;
|
> main {
|
||||||
align-items: center;
|
> div:first-of-type {
|
||||||
height: 100%;
|
width: 96%;
|
||||||
overflow: auto;
|
max-width: 750px;
|
||||||
}
|
margin: 0 auto;
|
||||||
|
}
|
||||||
&.isStandardWidth {
|
|
||||||
> div {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.isFullWidth {
|
|
||||||
> div {
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> main {
|
.isFullWidth {
|
||||||
flex: 1;
|
width: 100%;
|
||||||
height: calc(100% - 50px);
|
margin: 0 auto;
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
.contentWrap {
|
> div {
|
||||||
padding: 24px 24px 96px;
|
> header {
|
||||||
|
justify-content: flex-start;
|
||||||
&.isStandardWidth {
|
|
||||||
width: 96%;
|
|
||||||
max-width: 750px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.isFullWidth {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 React from 'react';
|
||||||
import { Spin } from '@douyinfe/semi-ui';
|
import { Spin } from '@douyinfe/semi-ui';
|
||||||
import { useUser } from 'data/user';
|
|
||||||
import { Seo } from 'components/seo';
|
import { Seo } from 'components/seo';
|
||||||
import { DataRender } from 'components/data-render';
|
import { DataRender } from 'components/data-render';
|
||||||
import { useTemplate } from 'data/template';
|
import { useTemplate } from 'data/template';
|
||||||
import { Editor } from './editor';
|
import { ImageViewer } from 'components/image-viewer';
|
||||||
|
import { ReaderEditor } from 'tiptap/editor';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
templateId: string;
|
templateId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateReader: React.FC<IProps> = ({ templateId }) => {
|
export const TemplateReader: React.FC<IProps> = ({ templateId }) => {
|
||||||
const { user } = useUser();
|
|
||||||
const { data, loading, error } = useTemplate(templateId);
|
const { data, loading, error } = useTemplate(templateId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -25,9 +24,10 @@ export const TemplateReader: React.FC<IProps> = ({ templateId }) => {
|
||||||
error={error}
|
error={error}
|
||||||
normalContent={() => {
|
normalContent={() => {
|
||||||
return (
|
return (
|
||||||
<div style={{ fontSize: 16 }}>
|
<div id="js-template-reader" className="container">
|
||||||
<Seo title={data.title} />
|
<Seo title={data.title} />
|
||||||
<Editor user={user} data={data} loading={loading} error={error} />
|
<ReaderEditor content={data.content} />
|
||||||
|
<ImageViewer containerSelector={`#js-template-reader`} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -75,7 +75,7 @@ export const Tree = ({ data, docAsLink, getDocLink, parentIds, activeId, isShare
|
||||||
<a className={styles.left}>
|
<a className={styles.left}>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
ellipsis={{
|
ellipsis={{
|
||||||
showTooltip: { opts: { content: label, position: 'right' } },
|
showTooltip: { opts: { content: label, style: { wordBreak: 'break-all' }, position: 'right' } },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|
|
@ -29,7 +29,7 @@ export const useComments = (documentId) => {
|
||||||
mutate();
|
mutate();
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
[mutate]
|
[mutate, documentId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateComment = useCallback(
|
const updateComment = useCallback(
|
||||||
|
@ -41,7 +41,7 @@ export const useComments = (documentId) => {
|
||||||
mutate();
|
mutate();
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
[mutate]
|
[mutate, documentId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteComment = useCallback(
|
const deleteComment = useCallback(
|
||||||
|
|
|
@ -59,7 +59,7 @@ export const useDocumentDetail = (documentId, options = null) => {
|
||||||
mutate();
|
mutate();
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
[mutate]
|
[mutate, documentId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleStatus = useCallback(
|
const toggleStatus = useCallback(
|
||||||
|
@ -68,7 +68,7 @@ export const useDocumentDetail = (documentId, options = null) => {
|
||||||
mutate();
|
mutate();
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
[mutate]
|
[mutate, documentId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { data, loading, error, update, toggleStatus };
|
return { data, loading, error, update, toggleStatus };
|
||||||
|
@ -118,7 +118,7 @@ export const useDocumentStar = (documentId) => {
|
||||||
targetId: documentId,
|
targetId: documentId,
|
||||||
});
|
});
|
||||||
mutate();
|
mutate();
|
||||||
}, [mutate]);
|
}, [mutate, documentId]);
|
||||||
|
|
||||||
return { data, error, toggleStar };
|
return { data, error, toggleStar };
|
||||||
};
|
};
|
||||||
|
@ -198,7 +198,7 @@ export const useCollaborationDocument = (documentId) => {
|
||||||
mutate();
|
mutate();
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
[mutate]
|
[mutate, documentId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateUser = useCallback(
|
const updateUser = useCallback(
|
||||||
|
@ -210,7 +210,7 @@ export const useCollaborationDocument = (documentId) => {
|
||||||
mutate();
|
mutate();
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
[mutate]
|
[mutate, documentId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteUser = useCallback(
|
const deleteUser = useCallback(
|
||||||
|
@ -222,7 +222,7 @@ export const useCollaborationDocument = (documentId) => {
|
||||||
mutate();
|
mutate();
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
[mutate]
|
[mutate, documentId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { users: data, loading, error, addUser, updateUser, deleteUser };
|
return { users: data, loading, error, addUser, updateUser, deleteUser };
|
||||||
|
|
|
@ -62,12 +62,12 @@ export const useTemplate = (templateId) => {
|
||||||
mutate();
|
mutate();
|
||||||
return ret as unknown as ITemplate;
|
return ret as unknown as ITemplate;
|
||||||
},
|
},
|
||||||
[mutate]
|
[mutate, templateId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteTemplate = useCallback(async () => {
|
const deleteTemplate = useCallback(async () => {
|
||||||
await HttpClient.post(`/template/delete/${templateId}`);
|
await HttpClient.post(`/template/delete/${templateId}`);
|
||||||
}, []);
|
}, [templateId]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
|
|
|
@ -104,7 +104,7 @@ export const useWikiTocs = (wikiId) => {
|
||||||
mutate();
|
mutate();
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
[mutate]
|
[mutate, wikiId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { data, loading, error, refresh: mutate, update };
|
return { data, loading, error, refresh: mutate, update };
|
||||||
|
@ -143,7 +143,7 @@ export const useWikiDetail = (wikiId) => {
|
||||||
mutate();
|
mutate();
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
[mutate]
|
[mutate, wikiId]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -157,7 +157,7 @@ export const useWikiDetail = (wikiId) => {
|
||||||
mutate();
|
mutate();
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
[mutate]
|
[mutate, wikiId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { data, loading, error, update, toggleStatus };
|
return { data, loading, error, update, toggleStatus };
|
||||||
|
@ -178,7 +178,7 @@ export const useWikiUsers = (wikiId) => {
|
||||||
mutate();
|
mutate();
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
[mutate]
|
[mutate, wikiId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateUser = useCallback(
|
const updateUser = useCallback(
|
||||||
|
@ -187,7 +187,7 @@ export const useWikiUsers = (wikiId) => {
|
||||||
mutate();
|
mutate();
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
[mutate]
|
[mutate, wikiId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteUser = useCallback(
|
const deleteUser = useCallback(
|
||||||
|
@ -196,7 +196,7 @@ export const useWikiUsers = (wikiId) => {
|
||||||
mutate();
|
mutate();
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
[mutate]
|
[mutate, wikiId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -229,7 +229,7 @@ export const useWikiStar = (wikiId) => {
|
||||||
targetId: wikiId,
|
targetId: wikiId,
|
||||||
});
|
});
|
||||||
mutate();
|
mutate();
|
||||||
}, [mutate]);
|
}, [mutate, wikiId]);
|
||||||
|
|
||||||
return { data, error, toggleStar };
|
return { data, error, toggleStar };
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,9 +2,10 @@ import type { AppProps } from 'next/app';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTheme } from 'hooks/use-theme';
|
import { useTheme } from 'hooks/use-theme';
|
||||||
|
import 'tiptap/fix-match-nodes';
|
||||||
import 'viewerjs/dist/viewer.css';
|
import 'viewerjs/dist/viewer.css';
|
||||||
import 'styles/globals.scss';
|
import 'styles/globals.scss';
|
||||||
import 'tiptap/styles/index.scss';
|
import 'tiptap/core/styles/index.scss';
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }: AppProps) {
|
function MyApp({ Component, pageProps }: AppProps) {
|
||||||
useTheme();
|
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;
|
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 { Node, mergeAttributes } from '@tiptap/core';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||||
import { AttachmentWrapper } from 'tiptap/wrappers/attachment';
|
import { AttachmentWrapper } from 'tiptap/core/wrappers/attachment';
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
interface Commands<ReturnType> {
|
interface Commands<ReturnType> {
|
|
@ -1,7 +1,6 @@
|
||||||
import { Blockquote as BuiltInBlockquote } from '@tiptap/extension-blockquote';
|
import { Blockquote as BuiltInBlockquote } from '@tiptap/extension-blockquote';
|
||||||
import { wrappingInputRule } from '@tiptap/core';
|
import { wrappingInputRule } from '@tiptap/core';
|
||||||
import { getParents } from 'tiptap/prose-utils';
|
import { getParents, getMarkdownSource } from 'tiptap/prose-utils';
|
||||||
import { getMarkdownSource } from 'tiptap/markdown/markdown-to-prosemirror';
|
|
||||||
|
|
||||||
export const Blockquote = BuiltInBlockquote.extend({
|
export const Blockquote = BuiltInBlockquote.extend({
|
||||||
addAttributes() {
|
addAttributes() {
|
|
@ -1,5 +1,5 @@
|
||||||
import { BulletList as BuiltInBulletList } from '@tiptap/extension-bullet-list';
|
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({
|
export const BulletList = BuiltInBulletList.extend({
|
||||||
addAttributes() {
|
addAttributes() {
|
|
@ -1,6 +1,6 @@
|
||||||
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
import { CalloutWrapper } from 'tiptap/wrappers/callout';
|
import { CalloutWrapper } from 'tiptap/core/wrappers/callout';
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
interface Commands<ReturnType> {
|
interface Commands<ReturnType> {
|
|
@ -3,7 +3,7 @@ import { Plugin, PluginKey, TextSelection } from 'prosemirror-state';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
import { lowlight } from 'lowlight/lib/all';
|
import { lowlight } from 'lowlight/lib/all';
|
||||||
import { LowlightPlugin } from 'tiptap/prose-utils';
|
import { LowlightPlugin } from 'tiptap/prose-utils';
|
||||||
import { CodeBlockWrapper } from 'tiptap/wrappers/code-block';
|
import { CodeBlockWrapper } from 'tiptap/core/wrappers/code-block';
|
||||||
|
|
||||||
export interface CodeBlockOptions {
|
export interface CodeBlockOptions {
|
||||||
/**
|
/**
|
|
@ -1,5 +1,5 @@
|
||||||
import BuiltInCode from '@tiptap/extension-code';
|
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({
|
export const Code = BuiltInCode.extend({
|
||||||
excludes: null,
|
excludes: null,
|
|
@ -1,6 +1,6 @@
|
||||||
import { Node, mergeAttributes } from '@tiptap/core';
|
import { Node, mergeAttributes } from '@tiptap/core';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
import { CountdownWrapper } from 'tiptap/wrappers/countdown';
|
import { CountdownWrapper } from 'tiptap/core/wrappers/countdown';
|
||||||
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
|
@ -1,6 +1,6 @@
|
||||||
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
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';
|
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
|
@ -1,6 +1,6 @@
|
||||||
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
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';
|
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
|
@ -3,9 +3,9 @@ import { ReactRenderer } from '@tiptap/react';
|
||||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||||
import Suggestion from '@tiptap/suggestion';
|
import Suggestion from '@tiptap/suggestion';
|
||||||
import tippy from 'tippy.js';
|
import tippy from 'tippy.js';
|
||||||
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/constants';
|
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
||||||
import { EmojiList } from 'tiptap/wrappers/emoji-list';
|
import { EmojiList } from 'tiptap/core/wrappers/emoji-list';
|
||||||
import { emojiSearch, emojisToName } from 'tiptap/wrappers/emoji-list/emojis';
|
import { emojiSearch, emojisToName } from 'tiptap/core/wrappers/emoji-list/emojis';
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
interface Commands<ReturnType> {
|
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 { 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';
|
import { markInputRegex, extractMarkAttributesFromMatch } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
export const marks = [{ name: 'underline', tag: 'u' }];
|
export const marks = [{ name: 'underline', tag: 'u' }];
|
|
@ -1,6 +1,6 @@
|
||||||
import { Node, mergeAttributes } from '@tiptap/core';
|
import { Node, mergeAttributes } from '@tiptap/core';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
import { IframeWrapper } from 'tiptap/wrappers/iframe';
|
import { IframeWrapper } from 'tiptap/core/wrappers/iframe';
|
||||||
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
|
@ -1,6 +1,6 @@
|
||||||
import { Image as BuiltInImage } from '@tiptap/extension-image';
|
import { Image as BuiltInImage } from '@tiptap/extension-image';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
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'));
|
const resolveImageEl = (element) => (element.nodeName === 'IMG' ? element : element.querySelector('img'));
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core';
|
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
import { KatexWrapper } from 'tiptap/wrappers/katex';
|
import { KatexWrapper } from 'tiptap/core/wrappers/katex';
|
||||||
|
|
||||||
type IKatexAttrs = {
|
type IKatexAttrs = {
|
||||||
text?: string;
|
text?: string;
|
|
@ -1,6 +1,6 @@
|
||||||
import { Node } from '@tiptap/core';
|
import { Node } from '@tiptap/core';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
import { LoadingWrapper } from 'tiptap/wrappers/loading';
|
import { LoadingWrapper } from 'tiptap/core/wrappers/loading';
|
||||||
|
|
||||||
export const Loading = Node.create({
|
export const Loading = Node.create({
|
||||||
name: 'loading',
|
name: 'loading',
|
|
@ -3,7 +3,7 @@ import { ReactRenderer } from '@tiptap/react';
|
||||||
import tippy from 'tippy.js';
|
import tippy from 'tippy.js';
|
||||||
import { getUsers } from 'services/user';
|
import { getUsers } from 'services/user';
|
||||||
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||||
import { MentionList } from 'tiptap/wrappers/mention-list';
|
import { MentionList } from 'tiptap/core/wrappers/mention-list';
|
||||||
|
|
||||||
const suggestion = {
|
const suggestion = {
|
||||||
items: async ({ query }) => {
|
items: async ({ query }) => {
|
|
@ -1,7 +1,7 @@
|
||||||
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core';
|
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
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';
|
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
const DEFAULT_MIND_DATA = {
|
const DEFAULT_MIND_DATA = {
|
|
@ -1,5 +1,5 @@
|
||||||
import { OrderedList as BuiltInOrderedList } from '@tiptap/extension-ordered-list';
|
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({
|
export const OrderedList = BuiltInOrderedList.extend({
|
||||||
addAttributes() {
|
addAttributes() {
|
|
@ -1,16 +1,30 @@
|
||||||
import { Extension } from '@tiptap/core';
|
import { Extension } from '@tiptap/core';
|
||||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
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 { handleFileEvent, isInCode, LANGUAGES, isTitleNode } from 'tiptap/prose-utils';
|
||||||
import {
|
import { copyNode, isMarkdown, normalizeMarkdown } from 'tiptap/prose-utils';
|
||||||
isMarkdown,
|
|
||||||
normalizePastedMarkdown,
|
|
||||||
markdownToProsemirror,
|
|
||||||
prosemirrorToMarkdown,
|
|
||||||
} from 'tiptap/markdown/markdown-to-prosemirror';
|
|
||||||
import { copyNode } from 'tiptap/prose-utils';
|
|
||||||
import { safeJSONParse } from 'helpers/json';
|
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 => {
|
const isPureText = (content): boolean => {
|
||||||
if (!content) return false;
|
if (!content) return false;
|
||||||
|
|
||||||
|
@ -27,11 +41,25 @@ const isPureText = (content): boolean => {
|
||||||
return content['type'] === 'text';
|
return content['type'] === 'text';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Paste = Extension.create({
|
export const Paste = Extension.create<IPasteOptions>({
|
||||||
name: 'paste',
|
name: 'paste',
|
||||||
priority: EXTENSION_PRIORITY_HIGHEST,
|
priority: EXTENSION_PRIORITY_HIGHEST,
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
markdownToHTML: (arg) => arg,
|
||||||
|
markdownToProsemirror: (arg) => arg.content,
|
||||||
|
prosemirrorToMarkdown: (arg) => String(arg.content),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addStorage() {
|
||||||
|
return this.options;
|
||||||
|
},
|
||||||
|
|
||||||
addProseMirrorPlugins() {
|
addProseMirrorPlugins() {
|
||||||
const { editor } = this;
|
const extensionThis = this;
|
||||||
|
const { editor } = extensionThis;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
new Plugin({
|
new Plugin({
|
||||||
|
@ -41,9 +69,8 @@ export const Paste = Extension.create({
|
||||||
if (view.props.editable && !view.props.editable(view.state)) {
|
if (view.props.editable && !view.props.editable(view.state)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!event.clipboardData) return false;
|
if (!event.clipboardData) return false;
|
||||||
|
// 文件
|
||||||
const files = Array.from(event.clipboardData.files);
|
const files = Array.from(event.clipboardData.files);
|
||||||
if (files.length) {
|
if (files.length) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -53,12 +80,14 @@ export const Paste = Extension.create({
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { markdownToProsemirror } = extensionThis.options;
|
||||||
const text = event.clipboardData.getData('text/plain');
|
const text = event.clipboardData.getData('text/plain');
|
||||||
const html = event.clipboardData.getData('text/html');
|
const html = event.clipboardData.getData('text/html');
|
||||||
const vscode = event.clipboardData.getData('vscode-editor-data');
|
const vscode = event.clipboardData.getData('vscode-editor-data');
|
||||||
const node = event.clipboardData.getData('text/node');
|
const node = event.clipboardData.getData('text/node');
|
||||||
const markdownText = event.clipboardData.getData('text/markdown');
|
const markdownText = event.clipboardData.getData('text/markdown');
|
||||||
|
|
||||||
|
// 直接复制节点
|
||||||
if (node) {
|
if (node) {
|
||||||
const doc = safeJSONParse(node);
|
const doc = safeJSONParse(node);
|
||||||
const tr = view.state.tr;
|
const tr = view.state.tr;
|
||||||
|
@ -98,7 +127,7 @@ export const Paste = Extension.create({
|
||||||
const schema = view.props.state.schema;
|
const schema = view.props.state.schema;
|
||||||
const doc = markdownToProsemirror({
|
const doc = markdownToProsemirror({
|
||||||
schema,
|
schema,
|
||||||
content: normalizePastedMarkdown(markdownText || text),
|
content: normalizeMarkdown(markdownText || text),
|
||||||
hasTitle,
|
hasTitle,
|
||||||
});
|
});
|
||||||
let tr = view.state.tr;
|
let tr = view.state.tr;
|
||||||
|
@ -111,13 +140,11 @@ export const Paste = Extension.create({
|
||||||
view.dispatch(tr.scrollIntoView());
|
view.dispatch(tr.scrollIntoView());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text.length !== 0) {
|
if (text.length !== 0) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
view.dispatch(view.state.tr.insertText(text));
|
view.dispatch(view.state.tr.insertText(text));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
handleDrop: (view, event: any) => {
|
handleDrop: (view, event: any) => {
|
||||||
|
@ -171,7 +198,7 @@ export const Paste = Extension.create({
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const content = prosemirrorToMarkdown({
|
const content = extensionThis.options.prosemirrorToMarkdown({
|
||||||
content: doc,
|
content: doc,
|
||||||
});
|
});
|
||||||
return content;
|
return content;
|
|
@ -3,9 +3,9 @@ import { Node } from '@tiptap/core';
|
||||||
import { ReactRenderer } from '@tiptap/react';
|
import { ReactRenderer } from '@tiptap/react';
|
||||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||||
import Suggestion from '@tiptap/suggestion';
|
import Suggestion from '@tiptap/suggestion';
|
||||||
import { MenuList } from 'tiptap/wrappers/menu-list';
|
import { MenuList } from 'tiptap/core/wrappers/menu-list';
|
||||||
import { QUICK_INSERT_ITEMS } from 'tiptap/menus/quick-insert';
|
import { QUICK_INSERT_ITEMS } from 'tiptap/editor/menus/quick-insert';
|
||||||
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/constants';
|
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
||||||
|
|
||||||
export const QuickInsertPluginKey = new PluginKey('quickInsert');
|
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 { Plugin, PluginKey, NodeSelection, TextSelection, Selection, AllSelection } from 'prosemirror-state';
|
||||||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||||
import { getCurrentNode, isInCodeBlock, isInCallout } from 'tiptap/prose-utils';
|
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');
|
export const selectionPluginKey = new PluginKey('selection');
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Node, mergeAttributes } from '@tiptap/core';
|
import { Node, mergeAttributes } from '@tiptap/core';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
import { StatusWrapper } from 'tiptap/wrappers/status';
|
import { StatusWrapper } from 'tiptap/core/wrappers/status';
|
||||||
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
import { getDatasetAttribute } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
type IStatusAttrs = {
|
type IStatusAttrs = {
|
|
@ -2,7 +2,7 @@ import { wrappingInputRule } from '@tiptap/core';
|
||||||
import { TaskItem as BuiltInTaskItem } from '@tiptap/extension-task-item';
|
import { TaskItem as BuiltInTaskItem } from '@tiptap/extension-task-item';
|
||||||
import { Plugin } from 'prosemirror-state';
|
import { Plugin } from 'prosemirror-state';
|
||||||
import { findParentNodeClosestToPos } from 'prosemirror-utils';
|
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({
|
const CustomTaskItem = BuiltInTaskItem.extend({
|
||||||
parseHTML() {
|
parseHTML() {
|
|
@ -1,5 +1,5 @@
|
||||||
import { TaskList as BuiltInTaskList } from '@tiptap/extension-task-list';
|
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({
|
export const TaskList = BuiltInTaskList.extend({
|
||||||
parseHTML() {
|
parseHTML() {
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue