refactor: improve tiptap

pull/31/head
fantasticit 2022-05-04 00:52:33 +08:00
parent a85132b30c
commit 8cbfbd71d1
269 changed files with 1617 additions and 1527 deletions

View File

@ -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",

View File

@ -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}
/> />
); );
}; };

View File

@ -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);
}; };

View File

@ -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 = () => {

View File

@ -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);
indexdbProvider.on('synced', () => {
setStatus('loadCacheSuccess');
});
return () => {
destoryIndexdbProvider(documentId);
};
}, [documentId, provider, authority]);
useEffect(() => {
if (!editor) return; if (!editor) return;
const handler = (data) => editor.commands.setContent(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(() => {
const handler = () => {
const editor = $editor.current && $editor.current.getEditor();
if (!editor) return; if (!editor) return;
const handler = () => {
// 已经拦截过一次,不再拦截 // 已经拦截过一次,不再拦截
if ($hasShowUserSettingModal.current) return; if ($hasShowUserSettingModal.current) return;
@ -169,75 +86,20 @@ 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) => (
<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="我们已与您断开连接,您可以继续编辑文档。一旦重新连接,我们会自动重新提交数据。"
/> />
)}
{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 <DocumentUserSetting
visible={mentionUsersSettingVisible} visible={mentionUsersSettingVisible}
toggleVisible={toggleMentionUsersSettingVisible} toggleVisible={toggleMentionUsersSettingVisible}
@ -248,19 +110,4 @@ export const _Editor: React.FC<IProps> = ({ user: currentUser, documentId, autho
/> />
</div> </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;
});

View File

@ -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 {
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 { > div {
display: inline-flex;
align-items: center;
height: 100%;
overflow: auto;
}
}
> main { > main {
flex: 1;
height: calc(100% - 50px);
overflow: auto;
.contentWrap {
padding: 24px 24px 96px; padding: 24px 24px 96px;
}
}
&.isStandardWidth { &.isStandardWidth {
> div {
> main {
> div:first-of-type {
width: 96%; width: 96%;
max-width: 750px; max-width: 750px;
margin: 0 auto; margin: 0 auto;
} }
}
}
}
&.isFullWidth { &.isFullWidth {
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
> div {
> header {
justify-content: flex-start;
} }
} }
} }

View File

@ -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>
)} )}

View File

@ -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>
);
}}
/>
);
};

View File

@ -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
editable={false}
user={user}
id={documentId}
type="document"
renderInEditorPortal={renderAuthor}
onAwarenessUpdate={triggerJoinUser}
/>
)}
{user && (
<div className={styles.commentWrap}> <div className={styles.commentWrap}>
<CommentEditor documentId={document.id} /> <CommentEditor documentId={document.id} />
</div> </div>
</Editor>
)} )}
{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>
)} )}

View File

@ -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)} />
</>
);
};

View File

@ -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 (
<>
<Seo title={data.title} />
<div <div
id="js-share-document-editor-container"
className={cls(styles.editorWrap, editorWrapClassNames)} className={cls(styles.editorWrap, editorWrapClassNames)}
style={{ fontSize }} style={{ fontSize }}
id="js-share-document-editor-container"
> >
<DocumentContent <Seo title={data.title} />
document={data} <CollaborationEditor menubar={false} editable={false} user={null} id={documentId} type="document" />
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>
); );
}} }}
/> />

View File

@ -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);
};

View File

@ -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: [] },
}); });

View File

@ -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>
<MenuBar editor={editor} />
</div>
</header>
<main id="js-template-editor-container">
<div className={cls(styles.contentWrap, editorWrapClassNames)} style={{ fontSize }}> <div className={cls(styles.contentWrap, editorWrapClassNames)} style={{ fontSize }}>
<EditorContent editor={editor} /> <CollaborationEditor menubar editable user={user} id={data.id} type="template" onTitleUpdate={setTitle} />
</div> </div>
<BackTop target={() => document.querySelector('#js-template-editor-container')} />
</main>
</div> </div>
</main> </main>
</div> </div>

View File

@ -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 {
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 { > div {
display: inline-flex;
align-items: center;
height: 100%; height: 100%;
overflow: auto;
}
&.isStandardWidth { > div:first-of-type > main {
> div {
margin: 0 auto;
}
}
&.isFullWidth {
> div {
margin: 0;
}
}
}
> main {
flex: 1;
height: calc(100% - 50px);
overflow: auto;
.contentWrap {
padding: 24px 24px 96px; padding: 24px 24px 96px;
}
}
&.isStandardWidth { .isStandardWidth {
> div {
> main {
> div:first-of-type {
width: 96%; width: 96%;
max-width: 750px; max-width: 750px;
margin: 0 auto; margin: 0 auto;
} }
}
}
}
&.isFullWidth { .isFullWidth {
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
> div {
> header {
justify-content: flex-start;
} }
} }
} }

View File

@ -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;
};

View File

@ -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>
);
};

View File

@ -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;
}

View File

@ -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>
); );
}} }}

View File

@ -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}

View File

@ -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(

View File

@ -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 };

View File

@ -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,

View File

@ -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 };
}; };

View File

@ -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();

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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> {

View File

@ -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() {

View File

@ -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() {

View File

@ -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> {

View File

@ -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 {
/** /**

View File

@ -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,

View File

@ -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' {

View File

@ -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' {

View File

@ -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' {

View File

@ -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> {

View File

@ -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;
},
});

View File

@ -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' }];

View File

@ -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' {

View File

@ -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'));

View File

@ -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;

View File

@ -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',

View File

@ -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 }) => {

View File

@ -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 = {

View File

@ -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() {

View File

@ -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;

View File

@ -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');

View File

@ -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');

View File

@ -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 = {

View File

@ -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() {

View File

@ -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