mirror of https://github.com/fantasticit/think.git
feat: add support for offline edit
This commit is contained in:
parent
ca7f18e640
commit
2ad6518a51
|
@ -78,6 +78,7 @@
|
||||||
"scroll-into-view-if-needed": "^2.2.29",
|
"scroll-into-view-if-needed": "^2.2.29",
|
||||||
"swr": "^1.2.0",
|
"swr": "^1.2.0",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
|
"y-indexeddb": "^9.0.7",
|
||||||
"y-prosemirror": "^1.0.14",
|
"y-prosemirror": "^1.0.14",
|
||||||
"yjs": "^13.5.24"
|
"yjs": "^13.5.24"
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { Banner as SemiBanner } from '@douyinfe/semi-ui';
|
||||||
|
import { BannerProps } from '@douyinfe/semi-ui/banner';
|
||||||
|
import { useToggle } from 'hooks/useToggle';
|
||||||
|
|
||||||
|
interface IProps extends BannerProps {
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Banner: React.FC<IProps> = ({ type, description, duration }) => {
|
||||||
|
const timer = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
const [visible, toggleVisible] = useToggle(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
clearTimeout(timer.current);
|
||||||
|
if (duration <= 0) return;
|
||||||
|
|
||||||
|
timer.current = setTimeout(() => {
|
||||||
|
toggleVisible(false);
|
||||||
|
}, duration);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer.current);
|
||||||
|
};
|
||||||
|
}, [duration]);
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
return <SemiBanner type="success" description="以为您恢复上一次离线时编辑数据。 " />;
|
||||||
|
};
|
|
@ -1,20 +1,24 @@
|
||||||
import React, { useMemo, useEffect } from 'react';
|
import React, { useMemo, useEffect, useState } from 'react';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
import { useEditor, EditorContent } from '@tiptap/react';
|
||||||
import { BackTop } from '@douyinfe/semi-ui';
|
import { BackTop } from '@douyinfe/semi-ui';
|
||||||
import { ILoginUser, IAuthority } from '@think/domains';
|
import { ILoginUser, IAuthority } from '@think/domains';
|
||||||
import { useToggle } from 'hooks/useToggle';
|
import { useToggle } from 'hooks/useToggle';
|
||||||
import {
|
import {
|
||||||
|
MenuBar,
|
||||||
DEFAULT_EXTENSION,
|
DEFAULT_EXTENSION,
|
||||||
DocumentWithTitle,
|
DocumentWithTitle,
|
||||||
getCollaborationExtension,
|
getCollaborationExtension,
|
||||||
getCollaborationCursorExtension,
|
getCollaborationCursorExtension,
|
||||||
getProvider,
|
getProvider,
|
||||||
destoryProvider,
|
destoryProvider,
|
||||||
MenuBar,
|
ProviderStatus,
|
||||||
|
getIndexdbProvider,
|
||||||
|
destoryIndexdbProvider,
|
||||||
} from 'components/tiptap';
|
} from 'components/tiptap';
|
||||||
import { DataRender } from 'components/data-render';
|
import { DataRender } from 'components/data-render';
|
||||||
import { joinUser } from 'components/document/collaboration';
|
import { joinUser } from 'components/document/collaboration';
|
||||||
|
import { Banner } from 'components/banner';
|
||||||
import { debounce } from 'helpers/debounce';
|
import { debounce } from 'helpers/debounce';
|
||||||
import { changeTitle } from './index';
|
import { changeTitle } from './index';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
@ -29,7 +33,7 @@ interface IProps {
|
||||||
|
|
||||||
export const Editor: React.FC<IProps> = ({ user, documentId, authority, className, style }) => {
|
export const Editor: React.FC<IProps> = ({ user, documentId, authority, className, style }) => {
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
const [status, setStatus] = useState<ProviderStatus>('connecting');
|
||||||
const provider = useMemo(() => {
|
const provider = useMemo(() => {
|
||||||
return getProvider({
|
return getProvider({
|
||||||
targetId: documentId,
|
targetId: documentId,
|
||||||
|
@ -63,16 +67,23 @@ export const Editor: React.FC<IProps> = ({ user, documentId, authority, classNam
|
||||||
const [loading, toggleLoading] = useToggle(true);
|
const [loading, toggleLoading] = useToggle(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const indexdbProvider = getIndexdbProvider(documentId, provider.document);
|
||||||
|
|
||||||
|
indexdbProvider.on('synced', () => {
|
||||||
|
setStatus('loadCacheSuccess');
|
||||||
|
});
|
||||||
|
|
||||||
provider.on('synced', () => {
|
provider.on('synced', () => {
|
||||||
toggleLoading(false);
|
toggleLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
provider.on('status', async ({ status }) => {
|
provider.on('status', async ({ status }) => {
|
||||||
console.log('status', status);
|
setStatus(status);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
destoryProvider(provider, 'EDITOR');
|
destoryProvider(provider, 'EDITOR');
|
||||||
|
destoryIndexdbProvider(documentId);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -83,6 +94,13 @@ export const Editor: React.FC<IProps> = ({ user, documentId, authority, classNam
|
||||||
normalContent={() => {
|
normalContent={() => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.editorWrap}>
|
<div className={styles.editorWrap}>
|
||||||
|
{status === 'disconnected' && (
|
||||||
|
<Banner
|
||||||
|
type="warning"
|
||||||
|
description="我们已与您断开连接,您可以继续编辑文档。一旦重新连接,我们会自动重新提交数据。
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<header className={className}>
|
<header className={className}>
|
||||||
<div>
|
<div>
|
||||||
<MenuBar editor={editor} />
|
<MenuBar editor={editor} />
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { BaseKit } from './basekit';
|
||||||
export { getSchema } from '@tiptap/core';
|
export { getSchema } from '@tiptap/core';
|
||||||
export * from './menubar';
|
export * from './menubar';
|
||||||
export * from './provider';
|
export * from './provider';
|
||||||
|
export * from './indexdb';
|
||||||
export * from './skeleton';
|
export * from './skeleton';
|
||||||
|
|
||||||
export const DocumentWithTitle = Document.extend({
|
export const DocumentWithTitle = Document.extend({
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
// const provider = new IndexeddbPersistence(docName, ydoc);
|
||||||
|
|
||||||
|
// provider.on('synced', () => {
|
||||||
|
// console.log('content from the database is loaded');
|
||||||
|
// });
|
||||||
|
|
||||||
|
import { IndexeddbPersistence } from 'y-indexeddb';
|
||||||
|
|
||||||
|
const POOL = new Map();
|
||||||
|
|
||||||
|
export const getIndexdbProvider = (name, doc) => {
|
||||||
|
if (!POOL.has(name)) {
|
||||||
|
POOL.set(name, new IndexeddbPersistence(name, doc));
|
||||||
|
}
|
||||||
|
|
||||||
|
return POOL.get(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const destoryIndexdbProvider = (name) => {
|
||||||
|
const provider = POOL.get(name);
|
||||||
|
|
||||||
|
if (!provider) return;
|
||||||
|
|
||||||
|
provider.destroy();
|
||||||
|
POOL.delete(name);
|
||||||
|
};
|
|
@ -4,6 +4,8 @@ import { IUser } from '@think/domains';
|
||||||
const PROVIDER_POOL_READER = new Map();
|
const PROVIDER_POOL_READER = new Map();
|
||||||
const PROVIDER_POOL_EDITOR = new Map();
|
const PROVIDER_POOL_EDITOR = new Map();
|
||||||
|
|
||||||
|
export type ProviderStatus = 'connecting' | 'connected' | 'disconnected' | 'loadCacheSuccess';
|
||||||
|
|
||||||
export const getProvider = ({
|
export const getProvider = ({
|
||||||
targetId,
|
targetId,
|
||||||
token,
|
token,
|
||||||
|
|
|
@ -116,6 +116,7 @@ importers:
|
||||||
tippy.js: ^6.3.7
|
tippy.js: ^6.3.7
|
||||||
tsconfig-paths-webpack-plugin: ^3.5.2
|
tsconfig-paths-webpack-plugin: ^3.5.2
|
||||||
typescript: 4.5.5
|
typescript: 4.5.5
|
||||||
|
y-indexeddb: ^9.0.7
|
||||||
y-prosemirror: ^1.0.14
|
y-prosemirror: ^1.0.14
|
||||||
yjs: ^13.5.24
|
yjs: ^13.5.24
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -188,6 +189,7 @@ importers:
|
||||||
scroll-into-view-if-needed: 2.2.29
|
scroll-into-view-if-needed: 2.2.29
|
||||||
swr: 1.2.0_react@17.0.2
|
swr: 1.2.0_react@17.0.2
|
||||||
tippy.js: 6.3.7
|
tippy.js: 6.3.7
|
||||||
|
y-indexeddb: 9.0.7_yjs@13.5.24
|
||||||
y-prosemirror: 1.0.14_0fedec857d2fb730ad5b02a71124bf2a
|
y-prosemirror: 1.0.14_0fedec857d2fb730ad5b02a71124bf2a
|
||||||
yjs: 13.5.24
|
yjs: 13.5.24
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
@ -8500,6 +8502,15 @@ packages:
|
||||||
engines: {node: '>=0.4'}
|
engines: {node: '>=0.4'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/y-indexeddb/9.0.7_yjs@13.5.24:
|
||||||
|
resolution: {integrity: sha512-58rDlwtRgXucgR9Kxc49AepAR6uGNGfcvPPAMrmMfSvRBQ/tPnx6NoHNyrRkhbALEAjV9tPEAIP0a/KkVqAIyA==}
|
||||||
|
peerDependencies:
|
||||||
|
yjs: ^13.0.0
|
||||||
|
dependencies:
|
||||||
|
lib0: 0.2.47
|
||||||
|
yjs: 13.5.24
|
||||||
|
dev: false
|
||||||
|
|
||||||
/y-prosemirror/1.0.14_0fedec857d2fb730ad5b02a71124bf2a:
|
/y-prosemirror/1.0.14_0fedec857d2fb730ad5b02a71124bf2a:
|
||||||
resolution: {integrity: sha512-fJQn/XT+z/gks9sd64eB+Mf1UQP0d5SHQ8RwbrzIwYFW+FgU/nx8/hJm+nrBAoO9azHOY5aDeoffeLmOFXLD9w==}
|
resolution: {integrity: sha512-fJQn/XT+z/gks9sd64eB+Mf1UQP0d5SHQ8RwbrzIwYFW+FgU/nx8/hJm+nrBAoO9azHOY5aDeoffeLmOFXLD9w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
Loading…
Reference in New Issue