diff --git a/packages/client/src/components/banner/index.tsx b/packages/client/src/components/banner/index.tsx index 3be20174..4538cbab 100644 --- a/packages/client/src/components/banner/index.tsx +++ b/packages/client/src/components/banner/index.tsx @@ -1,13 +1,15 @@ import React, { useEffect, useRef } from 'react'; import { Banner as SemiBanner } from '@douyinfe/semi-ui'; +import { IconClose } from '@douyinfe/semi-icons'; import { BannerProps } from '@douyinfe/semi-ui/banner'; import { useToggle } from 'hooks/use-toggle'; interface IProps extends BannerProps { duration?: number; + closeable?: boolean; } -export const Banner: React.FC = ({ type, description, duration = 0 }) => { +export const Banner: React.FC = ({ type, description, duration = 0, closeable = true }) => { const timer = useRef>(); const [visible, toggleVisible] = useToggle(true); @@ -26,5 +28,5 @@ export const Banner: React.FC = ({ type, description, duration = 0 }) => if (!visible) return null; - return ; + return : null} />; }; diff --git a/packages/client/src/components/data-render/index.tsx b/packages/client/src/components/data-render/index.tsx index 0e3db44a..2ccb4d1c 100644 --- a/packages/client/src/components/data-render/index.tsx +++ b/packages/client/src/components/data-render/index.tsx @@ -59,6 +59,7 @@ export const DataRender: React.FC = ({ normalContent, }) => { if (error) { + console.log(error, errorContent); return runRender(errorContent, error); } diff --git a/packages/client/src/components/document/editor/editor.tsx b/packages/client/src/components/document/editor/editor.tsx index 9cfa0e4d..cff39f48 100644 --- a/packages/client/src/components/document/editor/editor.tsx +++ b/packages/client/src/components/document/editor/editor.tsx @@ -1,12 +1,14 @@ import Router from 'next/router'; import React, { useMemo, useEffect, useState, useRef } from 'react'; import cls from 'classnames'; -import { useEditor, EditorContent } from '@tiptap/react'; -import { BackTop, Toast } from '@douyinfe/semi-ui'; +import { BackTop, Toast, Spin, Typography } from '@douyinfe/semi-ui'; import { ILoginUser, IAuthority } from '@think/domains'; +import { SecureDocumentIllustration } from 'illustrations/secure-document'; import { useToggle } from 'hooks/use-toggle'; import { useNetwork } from 'hooks/use-network'; import { + useEditor, + EditorContent, MenuBar, BaseKit, DocumentWithTitle, @@ -36,6 +38,8 @@ interface IProps { style: React.CSSProperties; } +const { Text } = Typography; + export const Editor: React.FC = ({ user: currentUser, documentId, authority, className, style }) => { const $hasShowUserSettingModal = useRef(false); const { users, addUser, updateUser } = useCollaborationDocument(documentId); @@ -182,7 +186,31 @@ export const Editor: React.FC = ({ user: currentUser, documentId, author return ( + + {/* FIXME: semi-design 的问题,不加 div,文字会换行! */} +
+
+ + } error={error} + errorContent={(error) => ( +
+ + + {(error && error.message) || '未知错误'} + +
+ )} normalContent={() => { return (
@@ -192,6 +220,9 @@ export const Editor: React.FC = ({ user: currentUser, documentId, author description="我们已与您断开连接,您可以继续编辑文档。一旦重新连接,我们会自动重新提交数据。" /> )} + {authority && !authority.editable && ( + + )}
diff --git a/packages/client/src/components/document/editor/index.tsx b/packages/client/src/components/document/editor/index.tsx index 406c3af2..7191b902 100644 --- a/packages/client/src/components/document/editor/index.tsx +++ b/packages/client/src/components/document/editor/index.tsx @@ -5,6 +5,7 @@ import { IconChevronLeft, IconArticle } from '@douyinfe/semi-icons'; import { useUser } from 'data/user'; import { useDocumentDetail } from 'data/document'; import { useWindowSize } from 'hooks/use-window-size'; +import { SecureDocumentIllustration } from 'illustrations/secure-document'; import { Seo } from 'components/seo'; import { Theme } from 'components/theme'; import { DataRender } from 'components/data-render'; @@ -43,26 +44,6 @@ export const DocumentEditor: React.FC = ({ documentId }) => { }); }, [document, documentId]); - const DocumentTitle = ( - <> - - + +
+ ); + } + return {error.message || error || '未知错误'}; + }} loadingContent={
diff --git a/packages/client/src/components/document/reader/user.tsx b/packages/client/src/components/document/reader/user.tsx index 7afe4b48..c8a76c04 100644 --- a/packages/client/src/components/document/reader/user.tsx +++ b/packages/client/src/components/document/reader/user.tsx @@ -1,5 +1,5 @@ import { createPortal } from 'react-dom'; -import { Space, Typography, Avatar } from '@douyinfe/semi-ui'; +import { Space, Avatar } from '@douyinfe/semi-ui'; import { IconUser } from '@douyinfe/semi-icons'; import { IDocument } from '@think/domains'; import { LocaleTime } from 'components/locale-time'; diff --git a/packages/client/src/components/template/editor/editor.tsx b/packages/client/src/components/template/editor/editor.tsx index 0b380ddf..9d22935c 100644 --- a/packages/client/src/components/template/editor/editor.tsx +++ b/packages/client/src/components/template/editor/editor.tsx @@ -1,15 +1,12 @@ import React, { useMemo, useCallback, useState, useEffect } from 'react'; import Router from 'next/router'; import cls from 'classnames'; -import { useEditor, EditorContent } from '@tiptap/react'; import { Button, Nav, Space, - Skeleton, Typography, Tooltip, - Spin, Switch, Popover, Popconfirm, @@ -19,8 +16,15 @@ import { import { IconChevronLeft, IconArticle } from '@douyinfe/semi-icons'; import { ILoginUser, ITemplate } from '@think/domains'; import { Theme } from 'components/theme'; -import { BaseKit, DocumentWithTitle, getCollaborationExtension, getProvider, MenuBar } from 'tiptap'; -import { DataRender } from 'components/data-render'; +import { + useEditor, + EditorContent, + BaseKit, + DocumentWithTitle, + getCollaborationExtension, + getProvider, + MenuBar, +} from 'tiptap'; import { User } from 'components/user'; import { DocumentStyle } from 'components/document/style'; import { LogoName } from 'components/logo'; @@ -33,13 +37,11 @@ const { Text } = Typography; interface IProps { user: ILoginUser; data: ITemplate; - loading: boolean; - error: Error | null; updateTemplate: (arg) => Promise; deleteTemplate: () => Promise; } -export const Editor: React.FC = ({ user, data, loading, error, updateTemplate, deleteTemplate }) => { +export const Editor: React.FC = ({ user, data, updateTemplate, deleteTemplate }) => { const { width: windowWidth } = useWindowSize(); const [title, setTitle] = useState(data.title); const provider = useMemo(() => { @@ -50,19 +52,22 @@ export const Editor: React.FC = ({ user, data, loading, error, updateTem user, docType: 'template', }); - }, [data, user]); - 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) { - // - } + }, []); + const editor = useEditor( + { + editable: true, + extensions: [...BaseKit, DocumentWithTitle, getCollaborationExtension(provider)], + onTransaction: ({ transaction }) => { + try { + const title = transaction.doc.content.firstChild.content.firstChild.textContent; + setTitle(title); + } catch (e) { + // + } + }, }, - }); + [provider] + ); const [isPublic, setPublic] = useState(false); const { width, fontSize } = useDocumentStyle(); const editorWrapClassNames = useMemo(() => { @@ -100,8 +105,6 @@ export const Editor: React.FC = ({ user, data, loading, error, updateTem }; }, []); - if (!user) return null; - return (
@@ -109,27 +112,14 @@ export const Editor: React.FC = ({ user, data, loading, error, updateTem style={{ overflow: 'auto' }} mode="horizontal" header={ - } - loading={true} - /> - } - normalContent={() => ( - <> - -
- - +
+
+
+
- } - error={error} - normalContent={() => { - return ( -
-
-
- -
-
-
-
- -
- document.querySelector('#js-template-editor-container')} /> -
-
- ); - }} - /> +
+
+
+ +
+ document.querySelector('#js-template-editor-container')} /> +
+
); diff --git a/packages/client/src/components/template/editor/index.tsx b/packages/client/src/components/template/editor/index.tsx index 7fbe41f0..e50cf347 100644 --- a/packages/client/src/components/template/editor/index.tsx +++ b/packages/client/src/components/template/editor/index.tsx @@ -27,14 +27,9 @@ export const TemplateEditor: React.FC = ({ templateId }) => { return ( <> - + {user && data && ( + + )} ); }} diff --git a/packages/client/src/components/template/reader/editor.tsx b/packages/client/src/components/template/reader/editor.tsx index 8d772fcf..fff033ef 100644 --- a/packages/client/src/components/template/reader/editor.tsx +++ b/packages/client/src/components/template/reader/editor.tsx @@ -1,9 +1,8 @@ import React, { useMemo } from 'react'; import cls from 'classnames'; -import { useEditor, EditorContent } from '@tiptap/react'; import { Layout, Spin, Typography } from '@douyinfe/semi-ui'; import { IUser, ITemplate } from '@think/domains'; -import { BaseKit, DocumentWithTitle } from 'tiptap'; +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'; @@ -21,21 +20,28 @@ interface IProps { } export const Editor: React.FC = ({ user, data, loading, error }) => { - const c = safeJSONParse(data.content); - let json = c.default || c; + 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), - }; - } + if (json && json.content) { + json = { + type: 'doc', + content: json.content.slice(1), + }; + } - const editor = useEditor({ - editable: false, - extensions: [...BaseKit, DocumentWithTitle], - content: json, - }); + return json; + }, [data]); + + const editor = useEditor( + { + editable: false, + extensions: [...BaseKit, DocumentWithTitle], + content: json, + }, + [json] + ); const { width, fontSize } = useDocumentStyle(); const editorWrapClassNames = useMemo(() => { diff --git a/packages/client/src/data/comment.ts b/packages/client/src/data/comment.ts index d2f68242..324b4ded 100644 --- a/packages/client/src/data/comment.ts +++ b/packages/client/src/data/comment.ts @@ -1,5 +1,5 @@ import type { IComment } from '@think/domains'; -import { useState } from 'react'; +import React, { useState, useCallback } from 'react'; import useSWR from 'swr'; import { HttpClient } from 'services/http-client'; @@ -20,29 +20,38 @@ export const useComments = (documentId) => { }>(`/comment/document/${documentId}?page=${page}`, (url) => HttpClient.get(url)); const loading = !data && !error; - const addComment = async (data: CreateCommentDto) => { - const ret = await HttpClient.post(`/comment/add`, { - documentId, - ...data, - }); - mutate(); - return ret; - }; + const addComment = useCallback( + async (data: CreateCommentDto) => { + const ret = await HttpClient.post(`/comment/add`, { + documentId, + ...data, + }); + mutate(); + return ret; + }, + [mutate] + ); - const updateComment = async (data: UpdateCommentDto) => { - const ret = await HttpClient.post(`/comment/update`, { - documentId, - ...data, - }); - mutate(); - return ret; - }; + const updateComment = useCallback( + async (data: UpdateCommentDto) => { + const ret = await HttpClient.post(`/comment/update`, { + documentId, + ...data, + }); + mutate(); + return ret; + }, + [mutate] + ); - const deleteComment = async (comment: IComment) => { - const ret = await HttpClient.post(`/comment/delete/${comment.id}`); - mutate(); - return ret; - }; + const deleteComment = useCallback( + async (comment: IComment) => { + const ret = await HttpClient.post(`/comment/delete/${comment.id}`); + mutate(); + return ret; + }, + [mutate] + ); return { data, diff --git a/packages/client/src/data/document.ts b/packages/client/src/data/document.ts index 077e572e..af888563 100644 --- a/packages/client/src/data/document.ts +++ b/packages/client/src/data/document.ts @@ -53,17 +53,23 @@ export const useDocumentDetail = (documentId, options = null) => { options ); const loading = !data && !error; - const update = async (data: IUpdateDocument) => { - const res = await HttpClient.post('/document/update/' + documentId, data); - mutate(); - return res; - }; + const update = useCallback( + async (data: IUpdateDocument) => { + const res = await HttpClient.post('/document/update/' + documentId, data); + mutate(); + return res; + }, + [mutate] + ); - const toggleStatus = async (data: Partial>) => { - const ret = await HttpClient.post('/document/share/' + documentId, data); - mutate(); - return ret; - }; + const toggleStatus = useCallback( + async (data: Partial>) => { + const ret = await HttpClient.post('/document/share/' + documentId, data); + mutate(); + return ret; + }, + [mutate] + ); return { data, loading, error, update, toggleStatus }; }; @@ -106,13 +112,13 @@ export const useDocumentStar = (documentId) => { }) ); - const toggleStar = async () => { + const toggleStar = useCallback(async () => { await HttpClient.post('/collector/toggle/', { type: 'document', targetId: documentId, }); mutate(); - }; + }, [mutate]); return { data, error, toggleStar }; }; @@ -181,34 +187,43 @@ export const useCollaborationDocument = (documentId) => { ); const loading = !data && !error; - const addUser = async (userName) => { - const ret = await HttpClient.post(`/document/user/${documentId}/add`, { - documentId, - userName, - readable: true, - editable: false, - }); - mutate(); - return ret; - }; + const addUser = useCallback( + async (userName) => { + const ret = await HttpClient.post(`/document/user/${documentId}/add`, { + documentId, + userName, + readable: true, + editable: false, + }); + mutate(); + return ret; + }, + [mutate] + ); - const updateUser = async (docAuth: DocAuth) => { - const ret = await HttpClient.post(`/document/user/${documentId}/update`, { - documentId, - ...docAuth, - }); - mutate(); - return ret; - }; + const updateUser = useCallback( + async (docAuth: DocAuth) => { + const ret = await HttpClient.post(`/document/user/${documentId}/update`, { + documentId, + ...docAuth, + }); + mutate(); + return ret; + }, + [mutate] + ); - const deleteUser = async (docAuth: DocAuth) => { - const ret = await HttpClient.post(`/document/user/${documentId}/delete`, { - documentId, - ...docAuth, - }); - mutate(); - return ret; - }; + const deleteUser = useCallback( + async (docAuth: DocAuth) => { + const ret = await HttpClient.post(`/document/user/${documentId}/delete`, { + documentId, + ...docAuth, + }); + mutate(); + return ret; + }, + [mutate] + ); return { users: data, loading, error, addUser, updateUser, deleteUser }; }; diff --git a/packages/client/src/data/message.ts b/packages/client/src/data/message.ts index 6d2db7c7..92b25be6 100644 --- a/packages/client/src/data/message.ts +++ b/packages/client/src/data/message.ts @@ -1,5 +1,5 @@ import type { IMessage } from '@think/domains'; -import { useState } from 'react'; +import React, { useState, useCallback } from 'react'; import useSWR from 'swr'; import { HttpClient } from 'services/http-client'; @@ -63,11 +63,14 @@ export const useUnreadMessages = () => { }); const loading = !data && !error; - const readMessage = async (messageId) => { - const ret = await HttpClient.post(`/message/read/${messageId}`); - mutate(); - return ret; - }; + const readMessage = useCallback( + async (messageId) => { + const ret = await HttpClient.post(`/message/read/${messageId}`); + mutate(); + return ret; + }, + [mutate] + ); return { data, diff --git a/packages/client/src/data/template.ts b/packages/client/src/data/template.ts index 7c82612f..b2fa106c 100644 --- a/packages/client/src/data/template.ts +++ b/packages/client/src/data/template.ts @@ -1,5 +1,5 @@ import type { ITemplate } from '@think/domains'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import useSWR from 'swr'; import { HttpClient } from 'services/http-client'; @@ -31,11 +31,14 @@ export const useOwnTemplates = () => { }>(`/template/own?page=${page}`, (url) => HttpClient.get(url)); const loading = !data && !error; - const addTemplate = async (data): Promise => { - const ret = await HttpClient.post(`/template/add`, data); - mutate(); - return ret as unknown as ITemplate; - }; + const addTemplate = useCallback( + async (data): Promise => { + const ret = await HttpClient.post(`/template/add`, data); + mutate(); + return ret as unknown as ITemplate; + }, + [mutate] + ); return { data, @@ -47,23 +50,24 @@ export const useOwnTemplates = () => { }; export const useTemplate = (templateId) => { - const { data, error, mutate } = useSWR(`/template/detail/${templateId}`, (url) => HttpClient.get(url), { - revalidateOnMount: true, - }); + const { data, error, mutate } = useSWR(`/template/detail/${templateId}`, (url) => HttpClient.get(url)); const loading = !data && !error; - const updateTemplate = async (data): Promise => { - const ret = await HttpClient.post(`/template/update`, { - id: templateId, - ...data, - }); - mutate(); - return ret as unknown as ITemplate; - }; + const updateTemplate = useCallback( + async (data): Promise => { + const ret = await HttpClient.post(`/template/update`, { + id: templateId, + ...data, + }); + mutate(); + return ret as unknown as ITemplate; + }, + [mutate] + ); - const deleteTemplate = async () => { + const deleteTemplate = useCallback(async () => { await HttpClient.post(`/template/delete/${templateId}`); - }; + }, []); return { data, diff --git a/packages/client/src/data/wiki.tsx b/packages/client/src/data/wiki.tsx index 460c2973..3be1ea6b 100644 --- a/packages/client/src/data/wiki.tsx +++ b/packages/client/src/data/wiki.tsx @@ -1,6 +1,6 @@ import { CollectType, IDocument, IUser, IWiki, IWikiUser } from '@think/domains'; import useSWR from 'swr'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { HttpClient } from 'services/http-client'; export type ICreateWiki = Pick; @@ -47,22 +47,28 @@ export const useOwnWikis = () => { HttpClient.get(url) ); - const createWiki = async (data: ICreateWiki) => { - const res = await HttpClient.post('/wiki/create', data); - mutate(); - return res; - }; + const createWiki = useCallback( + async (data: ICreateWiki) => { + const res = await HttpClient.post('/wiki/create', data); + mutate(); + return res; + }, + [mutate] + ); /** * 删除文档 * @param id * @returns */ - const deletWiki = async (id) => { - const res = await HttpClient.delete('/wiki/delete/' + id); - mutate(); - return res; - }; + const deletWiki = useCallback( + async (id) => { + const res = await HttpClient.delete('/wiki/delete/' + id); + mutate(); + return res; + }, + [mutate] + ); const loading = !data && !error; const list = (data && data.data) || []; @@ -92,11 +98,14 @@ export const useWikiTocs = (wikiId) => { ); const loading = !data && !error; - const update = async (relations: Array<{ id: string; parentDocumentId: string }>) => { - const res = await HttpClient.post(`/wiki/tocs/${wikiId}/update`, relations); - mutate(); - return res; - }; + const update = useCallback( + async (relations: Array<{ id: string; parentDocumentId: string }>) => { + const res = await HttpClient.post(`/wiki/tocs/${wikiId}/update`, relations); + mutate(); + return res; + }, + [mutate] + ); return { data, loading, error, refresh: mutate, update }; }; @@ -128,22 +137,28 @@ export const useWikiDetail = (wikiId) => { * @param data * @returns */ - const update = async (data: IUpdateWiki) => { - const res = await HttpClient.patch('/wiki/update/' + wikiId, data); - mutate(); - return res; - }; + const update = useCallback( + async (data: IUpdateWiki) => { + const res = await HttpClient.patch('/wiki/update/' + wikiId, data); + mutate(); + return res; + }, + [mutate] + ); /** * 公开或私有知识库 * @param data * @returns */ - const toggleStatus = async (data) => { - const res = await HttpClient.post('/wiki/share/' + wikiId, data); - mutate(); - return res; - }; + const toggleStatus = useCallback( + async (data) => { + const res = await HttpClient.post('/wiki/share/' + wikiId, data); + mutate(); + return res; + }, + [mutate] + ); return { data, loading, error, update, toggleStatus }; }; @@ -157,23 +172,32 @@ export const useWikiUsers = (wikiId) => { const { data, error, mutate } = useSWR('/wiki/user/' + wikiId, (url) => HttpClient.get(url)); const loading = !data && !error; - const addUser = async (data: IWikiUserOpeateData) => { - const ret = await HttpClient.post(`/wiki/user/${wikiId}/add`, data); - mutate(); - return ret; - }; + const addUser = useCallback( + async (data: IWikiUserOpeateData) => { + const ret = await HttpClient.post(`/wiki/user/${wikiId}/add`, data); + mutate(); + return ret; + }, + [mutate] + ); - const updateUser = async (data: IWikiUserOpeateData) => { - const ret = await HttpClient.post(`/wiki/user/${wikiId}/update`, data); - mutate(); - return ret; - }; + const updateUser = useCallback( + async (data: IWikiUserOpeateData) => { + const ret = await HttpClient.post(`/wiki/user/${wikiId}/update`, data); + mutate(); + return ret; + }, + [mutate] + ); - const deleteUser = async (data: IWikiUserOpeateData) => { - const ret = await HttpClient.post(`/wiki/user/${wikiId}/delete`, data); - mutate(); - return ret; - }; + const deleteUser = useCallback( + async (data: IWikiUserOpeateData) => { + const ret = await HttpClient.post(`/wiki/user/${wikiId}/delete`, data); + mutate(); + return ret; + }, + [mutate] + ); return { data, @@ -199,13 +223,13 @@ export const useWikiStar = (wikiId) => { }) ); - const toggleStar = async () => { + const toggleStar = useCallback(async () => { await HttpClient.post('/collector/toggle/', { type: CollectType.wiki, targetId: wikiId, }); mutate(); - }; + }, [mutate]); return { data, error, toggleStar }; }; diff --git a/packages/client/src/illustrations/secure-document.tsx b/packages/client/src/illustrations/secure-document.tsx new file mode 100644 index 00000000..b568b604 --- /dev/null +++ b/packages/client/src/illustrations/secure-document.tsx @@ -0,0 +1,188 @@ +import React from 'react'; + +export const SecureDocumentIllustration = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/client/src/tiptap/index.ts b/packages/client/src/tiptap/index.ts index 579a6142..303b5c9f 100644 --- a/packages/client/src/tiptap/index.ts +++ b/packages/client/src/tiptap/index.ts @@ -1,34 +1,7 @@ -import { HocuspocusProvider } from '@hocuspocus/provider'; -import { Collaboration } from './extensions/collaboration'; -import { CollaborationCursor } from './extensions/collaboration-cursor'; -import History from '@tiptap/extension-history'; -import { getRandomColor } from 'helpers/color'; -import { Document } from './extensions/document'; -export { BaseKit, CommentKit } from './start-kit'; - export { getSchema } from '@tiptap/core'; +export * from './react'; +export * from './start-kit'; export * from './menubar'; export * from './provider'; export * from './indexdb'; export * from './skeleton'; - -export const DocumentWithTitle = Document.extend({ - content: 'title block+', -}); - -export { Document, History }; - -export const getCollaborationExtension = (provider: HocuspocusProvider) => { - return Collaboration.configure({ - document: provider.document, - }); -}; -export const getCollaborationCursorExtension = (provider: HocuspocusProvider, user) => { - return CollaborationCursor.configure({ - provider, - user: { - ...user, - color: getRandomColor(), - }, - }); -}; diff --git a/packages/client/src/tiptap/menubar.tsx b/packages/client/src/tiptap/menubar.tsx index 86c6c989..ecb8c9b2 100644 --- a/packages/client/src/tiptap/menubar.tsx +++ b/packages/client/src/tiptap/menubar.tsx @@ -43,7 +43,11 @@ import { Iframe } from './menus/iframe'; import { Table } from './menus/table'; import { Mind } from './menus/mind'; +import useTilg from 'tilg'; + const _MenuBar: React.FC<{ editor: any }> = ({ editor }) => { + useTilg(); + if (!editor) return null; return ( diff --git a/packages/client/src/tiptap/react/index.tsx b/packages/client/src/tiptap/react/index.tsx new file mode 100644 index 00000000..35c04383 --- /dev/null +++ b/packages/client/src/tiptap/react/index.tsx @@ -0,0 +1,4 @@ +import { EditorContent, NodeViewWrapper, NodeViewContent } from '@tiptap/react'; +import { useEditor } from './useEditor'; + +export { EditorContent, NodeViewWrapper, NodeViewContent, useEditor }; diff --git a/packages/client/src/tiptap/react/useEditor.tsx b/packages/client/src/tiptap/react/useEditor.tsx new file mode 100644 index 00000000..eab529f8 --- /dev/null +++ b/packages/client/src/tiptap/react/useEditor.tsx @@ -0,0 +1,35 @@ +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 = {}, deps: DependencyList = []) => { + const [editor, setEditor] = useState(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; +}; diff --git a/packages/client/src/tiptap/start-kit.tsx b/packages/client/src/tiptap/start-kit.tsx index 599db37f..9b441e79 100644 --- a/packages/client/src/tiptap/start-kit.tsx +++ b/packages/client/src/tiptap/start-kit.tsx @@ -1,3 +1,4 @@ +import { HocuspocusProvider } from '@hocuspocus/provider'; import { Attachment } from './extensions/attachment'; import { BackgroundColor } from './extensions/background-color'; import { Blockquote } from './extensions/blockquote'; @@ -54,6 +55,15 @@ import { TrailingNode } from './extensions/trailing-node'; import { Underline } from './extensions/underline'; import { Paste } from './extensions/paste'; +import { getRandomColor } from 'helpers/color'; +// 文档 +import { Document } from './extensions/document'; +// 操作历史 +import History from '@tiptap/extension-history'; +// 协作 +import { Collaboration } from './extensions/collaboration'; +import { CollaborationCursor } from './extensions/collaboration-cursor'; + export const BaseKit = [ Attachment, BackgroundColor, @@ -164,3 +174,25 @@ export const CommentKit = [ TrailingNode, Underline, ]; + +export { Document, History }; + +export const DocumentWithTitle = Document.extend({ + content: 'title block+', +}); + +export const getCollaborationExtension = (provider: HocuspocusProvider) => { + return Collaboration.configure({ + document: provider.document, + }); +}; + +export const getCollaborationCursorExtension = (provider: HocuspocusProvider, user) => { + return CollaborationCursor.configure({ + provider, + user: { + ...user, + color: getRandomColor(), + }, + }); +}; diff --git a/packages/client/src/tiptap/views/bubble-menu/index.tsx b/packages/client/src/tiptap/views/bubble-menu/index.tsx index c57e8043..123ebd1d 100644 --- a/packages/client/src/tiptap/views/bubble-menu/index.tsx +++ b/packages/client/src/tiptap/views/bubble-menu/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef } from 'react'; import { BubbleMenuPlugin, BubbleMenuPluginProps } from './bubble-menu-plugin'; type Optional = Pick, K> & Omit; @@ -8,9 +8,11 @@ export type BubbleMenuProps = Omit, }; export const BubbleMenu: React.FC = (props) => { - const [element, setElement] = useState(null); + const $element = useRef(null); useEffect(() => { + const element = $element.current; + if (!element) { return; } @@ -41,10 +43,10 @@ export const BubbleMenu: React.FC = (props) => { editor.registerPlugin(plugin); return () => editor.unregisterPlugin(pluginKey); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.editor, element]); + }, [props.editor]); return ( -
+
{props.children}
);