Skip to content

Commit e87aa53

Browse files
authored
Refactor(collaborative-editing): 协同编辑模块依赖注入 (#380)
* Refactor: 协同编辑模块依赖注入 * chore: add collaborative editing dependencies to lock.yaml
1 parent 072d2f9 commit e87aa53

11 files changed

Lines changed: 216 additions & 61 deletions

File tree

packages/docs/fluent-editor/demos/collaborative-editing.vue

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,22 @@ onMounted(() => {
5151
Promise.all([
5252
import('@opentiny/fluent-editor'),
5353
import('quill-table-up'),
54+
import('yjs'),
55+
import('y-protocols/awareness'),
56+
import('y-quill'),
57+
import('y-websocket'),
58+
import('y-indexeddb'),
59+
import('quill-cursors'),
5460
]).then(
5561
([
5662
{ default: FluentEditor, generateTableUp, CollaborationModule },
5763
{ defaultCustomSelect, TableMenuContextmenu, TableSelection, TableUp },
64+
Y,
65+
{ Awareness },
66+
{ QuillBinding },
67+
{ WebsocketProvider },
68+
{ IndexeddbPersistence },
69+
{ default: QuillCursors },
5870
]) => {
5971
if (!editorRef.value) return
6072
@@ -93,6 +105,14 @@ onMounted(() => {
93105
},
94106
},
95107
'collaborative-editing': {
108+
deps: {
109+
Y,
110+
Awareness,
111+
QuillBinding,
112+
QuillCursors,
113+
WebsocketProvider,
114+
IndexeddbPersistence,
115+
},
96116
provider: {
97117
type: 'websocket',
98118
options: {

packages/docs/fluent-editor/docs/demo/collaborative-editing.md

Lines changed: 85 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
## 在线协同演示
1111

12-
整个协同编辑系统由三部分组成:前端 `TinyEditor`、中间层协作引擎 `Yjs` 和后端服务(用于数据同步和持久化)。前端编辑器将操作传递给 `Yjs``Yjs` 通过不同的连接协议(如 `WebSocket``WebRTC`)实现多端同步, 并支持将数据持久化到后端数据库(如 `MongoDB`)。
12+
整个协同编辑系统由三部分组成:前端 `TinyEditor`、中间层协作引擎 `Yjs` 和后端服务(用于数据同步和持久化)。前端编辑器将操作传递给 `Yjs``Yjs` 通过不同的连接协议(如 `WebSocket``WebRTC`)实现多端同步, 并支持将数据持久化到后端数据库(如 `MongoDB`).
13+
1314
<img src="/Collab-arch.png" alt="Tiny-editor-demo">
1415

1516
下面是一个完整的协同编辑演示:
@@ -29,29 +30,52 @@
2930
pnpm i quill-cursors y-protocols y-quill yjs y-indexeddb y-websocket
3031
```
3132

32-
引入协同编辑模块
33-
34-
```javascript
35-
import FluentEditor, { CollaborationModule } from '@opentiny/fluent-editor'
36-
FluentEditor.register('modules/collaborative-editing', CollaborationModule, true)
37-
```
38-
39-
编辑器基础配置:
33+
引入协同编辑模块和依赖
4034

4135
```javascript
42-
const editor = new FluentEditor('#editor', {
43-
theme: 'snow',
44-
modules: {
45-
'collaborative-editing': {
46-
provider: {
47-
type: 'websocket',
48-
options: {
49-
serverUrl: 'ws://localhost:1234',
50-
roomName: 'Tiny-Editor-Demo',
36+
import FluentEditor from '@opentiny/fluent-editor'
37+
38+
let editor
39+
const editorRef = document.querySelector('#editor')
40+
41+
Promise.all([
42+
import('@opentiny/fluent-editor'),
43+
import('yjs'),
44+
import('y-protocols/awareness'),
45+
import('y-quill'),
46+
import('y-websocket'),
47+
import('y-indexeddb'),
48+
import('quill-cursors'),
49+
]).then(
50+
([
51+
{ default: FluentEditor, CollaborationModule },
52+
Y,
53+
{ Awareness },
54+
{ QuillBinding },
55+
{ WebsocketProvider },
56+
{ IndexeddbPersistence },
57+
{ default: QuillCursors },
58+
]) => {
59+
FluentEditor.register('modules/collaborative-editing', CollaborationModule, true)
60+
61+
editor = new FluentEditor(editorRef, {
62+
theme: 'snow',
63+
modules: {
64+
'collaborative-editing': {
65+
deps: { Y, Awareness, QuillBinding, QuillCursors, WebsocketProvider, IndexeddbPersistence },
66+
provider: {
67+
type: 'websocket',
68+
options: {
69+
serverUrl: 'wss://demos.yjs.dev/ws',
70+
roomName: 'Tiny-Editor-Demo',
71+
},
72+
},
5173
},
5274
},
53-
},
75+
})
5476
},
77+
).catch((error) => {
78+
console.error('Failed to initialize FluentEditor:', error)
5579
})
5680
```
5781

@@ -192,6 +216,7 @@ const editor = new FluentEditor('#editor', {
192216
theme: 'snow',
193217
modules: {
194218
'collaborative-editing': {
219+
deps: { Y, Awareness, QuillBinding, QuillCursors, WebsocketProvider, IndexeddbPersistence },
195220
provider: {
196221
type: 'websocket',
197222
options: {
@@ -229,19 +254,49 @@ const editor = new FluentEditor('#editor', {
229254
#### WebRTC 前端配置示例
230255

231256
```javascript
232-
const editor = new FluentEditor('#editor', {
233-
theme: 'snow',
234-
modules: {
235-
'collaborative-editing': {
236-
provider: {
237-
type: 'webrtc',
238-
options: {
239-
roomName: 'Tiny-Editor-WebRTC',
240-
signaling: ['wss://signaling.yjs.dev'],
257+
import FluentEditor from '@opentiny/fluent-editor'
258+
259+
let editor
260+
const editorRef = document.querySelector('#editor')
261+
262+
Promise.all([
263+
import('@opentiny/fluent-editor'),
264+
import('yjs'),
265+
import('y-protocols/awareness'),
266+
import('y-quill'),
267+
import('y-webrtc'),
268+
import('y-indexeddb'),
269+
import('quill-cursors'),
270+
]).then(
271+
([
272+
{ default: FluentEditor, CollaborationModule },
273+
Y,
274+
{ Awareness },
275+
{ QuillBinding },
276+
{ WebrtcProvider },
277+
{ IndexeddbPersistence },
278+
{ default: QuillCursors },
279+
]) => {
280+
FluentEditor.register('modules/collaborative-editing', CollaborationModule, true)
281+
282+
editor = new FluentEditor(editorRef, {
283+
theme: 'snow',
284+
modules: {
285+
'collaborative-editing': {
286+
deps: { Y, Awareness, QuillBinding, QuillCursors, WebrtcProvider, IndexeddbPersistence },
287+
provider: {
288+
type: 'webrtc',
289+
options: {
290+
roomName: 'Tiny-Editor-WebRTC',
291+
signaling: ['wss://signaling.yjs.dev'],
292+
},
293+
},
241294
},
242295
},
243-
},
296+
})
244297
},
298+
).catch((error) => {
299+
console.error('Failed to initialize FluentEditor:', error)
245300
})
246301
```
247302

@@ -425,6 +480,7 @@ const editor = new FluentEditor('#editor', {
425480
theme: 'snow',
426481
modules: {
427482
'collaborative-editing': {
483+
deps: { Y, Awareness, QuillBinding, QuillCursors, WebsocketProvider, IndexeddbPersistence },
428484
cursors: {
429485
template: `
430486
<span class="${CURSOR_CLASSES.SELECTION_CLASS}"></span>

packages/docs/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,21 @@
2424
"html2canvas": "^1.4.1",
2525
"katex": "^0.16.11",
2626
"mathlive": "^0.101.0",
27+
"quill-cursors": "^4.0.3",
2728
"quill-header-list": "0.0.2",
2829
"quill-markdown-shortcuts": "^0.0.10",
2930
"quill-table-up": "^3.0.1",
3031
"quill-toolbar-tip": "^0.0.13",
3132
"simple-mind-map": "0.14.0-fix.1",
3233
"simple-mind-map-plugin-themes": "^1.0.1",
3334
"vue": "^3.5.13",
34-
"vue-toastification": "2.0.0-rc.5"
35+
"vue-toastification": "2.0.0-rc.5",
36+
"y-indexeddb": "^9.0.12",
37+
"y-protocols": "^1.0.6",
38+
"y-quill": "^1.0.0",
39+
"y-webrtc": "^10.3.0",
40+
"y-websocket": "^3.0.0",
41+
"yjs": "^13.6.27"
3542
},
3643
"devDependencies": {
3744
"@playwright/test": "^1.46.1",

packages/fluent-editor/src/modules/collaborative-editing/awareness/awareness.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type QuillCursors from 'quill-cursors'
22
import type { Awareness } from 'y-protocols/awareness'
3+
import type * as Y from 'yjs'
34
import type FluentEditor from '../../../core/fluent-editor'
4-
import * as Y from 'yjs'
55

66
export interface AwarenessState {
77
name?: string
@@ -37,6 +37,7 @@ export function bindAwarenessToCursors(
3737
cursorsModule: QuillCursors,
3838
quill: FluentEditor,
3939
yText: Y.Text,
40+
Yjs: typeof Y,
4041
): (() => void) | void {
4142
if (!cursorsModule || !awareness) return
4243

@@ -51,8 +52,8 @@ export function bindAwarenessToCursors(
5152

5253
cursorsModule.createCursor(clientId.toString(), name, color)
5354

54-
const anchor = Y.createAbsolutePositionFromRelativePosition(Y.createRelativePositionFromJSON(state.cursor.anchor), doc)
55-
const head = Y.createAbsolutePositionFromRelativePosition(Y.createRelativePositionFromJSON(state.cursor.head), doc)
55+
const anchor = Yjs.createAbsolutePositionFromRelativePosition(Yjs.createRelativePositionFromJSON(state.cursor.anchor), doc)
56+
const head = Yjs.createAbsolutePositionFromRelativePosition(Yjs.createRelativePositionFromJSON(state.cursor.head), doc)
5657

5758
if (anchor && head && anchor.type === yText && clientId) {
5859
setTimeout(() => {
@@ -75,13 +76,13 @@ export function bindAwarenessToCursors(
7576
const selectionChangeHandler = (range: { index: number, length: number } | null) => {
7677
setTimeout(() => {
7778
if (range) {
78-
const anchor = Y.createRelativePositionFromTypeIndex(yText, range.index)
79-
const head = Y.createRelativePositionFromTypeIndex(yText, range.index + range.length)
79+
const anchor = Yjs.createRelativePositionFromTypeIndex(yText, range.index)
80+
const head = Yjs.createRelativePositionFromTypeIndex(yText, range.index + range.length)
8081

8182
const currentState = awareness.getLocalState()
8283
if (!currentState?.cursor
83-
|| !Y.compareRelativePositions(anchor, currentState.cursor.anchor)
84-
|| !Y.compareRelativePositions(head, currentState.cursor.head)) {
84+
|| !Yjs.compareRelativePositions(anchor, currentState.cursor.anchor)
85+
|| !Yjs.compareRelativePositions(head, currentState.cursor.head)) {
8586
awareness.setLocalStateField('cursor', { anchor, head })
8687
}
8788
}

packages/fluent-editor/src/modules/collaborative-editing/awareness/y-indexeddb.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import type { Doc } from 'yjs'
2-
import { IndexeddbPersistence } from 'y-indexeddb'
1+
import type { IndexeddbPersistence } from 'y-indexeddb'
2+
import type * as Y from 'yjs'
3+
4+
export function setupIndexedDB(doc: Y.Doc, IndexeddbPersistenceClass?: typeof IndexeddbPersistence): () => void {
5+
if (!IndexeddbPersistenceClass) {
6+
console.warn('[yjs] IndexeddbPersistence not provided, offline support disabled')
7+
return () => {}
8+
}
39

4-
export function setupIndexedDB(doc: Doc): () => void {
510
const fullDbName = `tiny-editor-${doc.guid}`
611

7-
new IndexeddbPersistence(fullDbName, doc)
12+
new IndexeddbPersistenceClass(fullDbName, doc)
813

914
return (): void => {
1015
indexedDB.deleteDatabase(fullDbName)

packages/fluent-editor/src/modules/collaborative-editing/collaborative-editing.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,36 @@
1+
import type QuillCursors from 'quill-cursors'
2+
import type { Awareness } from 'y-protocols/awareness'
3+
import type * as Y from 'yjs'
14
import type FluentEditor from '../../fluent-editor'
25
import type { UnifiedProvider } from './provider/providerRegistry'
3-
import type { YjsOptions } from './types'
4-
import QuillCursors from 'quill-cursors'
5-
import { Awareness } from 'y-protocols/awareness'
6-
import { QuillBinding } from 'y-quill'
7-
import * as Y from 'yjs'
6+
import type { CollaborativeEditingDeps, YjsOptions } from './types'
87
import { bindAwarenessToCursors, setupAwareness } from './awareness'
98
import { setupIndexedDB } from './awareness/y-indexeddb'
109
import { createProvider } from './provider/providerRegistry'
1110

1211
export class CollaborativeEditor {
13-
private ydoc: Y.Doc = new Y.Doc()
12+
private ydoc: Y.Doc
1413
private provider: UnifiedProvider
1514
private awareness: Awareness
1615
private cursors: QuillCursors | null
1716
private cleanupBindings: (() => void) | null = null
1817
private clearIndexedDB: (() => void) | null = null
18+
private deps: CollaborativeEditingDeps
1919

2020
constructor(
2121
public quill: FluentEditor,
2222
public options: YjsOptions,
2323
) {
24+
this.deps = this.options.deps || (window as any)
25+
const { Y, Awareness, QuillBinding, QuillCursors } = this.deps
26+
27+
if (!Y || !Awareness || !QuillBinding || !QuillCursors) {
28+
throw new Error(
29+
'Missing required dependencies for collaborative editing. '
30+
+ 'Please provide Y, Awareness, QuillBinding, and QuillCursors in the deps option.',
31+
)
32+
}
33+
2434
this.ydoc = this.options.ydoc || new Y.Doc()
2535

2636
if (this.options.cursors !== false) {
@@ -51,6 +61,7 @@ export class CollaborativeEditor {
5161
onDisconnect: this.options.onDisconnect,
5262
onError: this.options.onError,
5363
onSyncChange: this.options.onSyncChange,
64+
deps: this.deps,
5465
})
5566
this.provider = provider
5667
}
@@ -64,7 +75,7 @@ export class CollaborativeEditor {
6475

6576
if (this.provider) {
6677
const ytext = this.ydoc.getText('tiny-editor')
67-
this.cleanupBindings = bindAwarenessToCursors(this.awareness, this.cursors, quill, ytext) || null
78+
this.cleanupBindings = bindAwarenessToCursors(this.awareness, this.cursors, quill, ytext, Y) || null
6879
new QuillBinding(
6980
ytext,
7081
this.quill,
@@ -76,7 +87,7 @@ export class CollaborativeEditor {
7687
}
7788

7889
if (this.options.offline !== false) {
79-
this.clearIndexedDB = setupIndexedDB(this.ydoc)
90+
this.clearIndexedDB = setupIndexedDB(this.ydoc, this.deps.IndexeddbPersistence)
8091
}
8192
}
8293

packages/fluent-editor/src/modules/collaborative-editing/provider/providerRegistry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Awareness } from 'y-protocols/awareness'
22
import type * as Y from 'yjs'
3-
import type { ProviderEventHandlers } from '../types'
3+
import type { CollaborativeEditingDeps, ProviderEventHandlers } from '../types'
44
import { WebRTCProviderWrapper } from './webrtc'
55
import { WebsocketProviderWrapper } from './websocket'
66

@@ -14,6 +14,7 @@ export type ProviderConstructorProps<T = any> = {
1414
options: T
1515
awareness?: Awareness
1616
doc?: Y.Doc
17+
deps?: CollaborativeEditingDeps
1718
} & ProviderEventHandlers
1819

1920
export interface UnifiedProvider extends ProviderEventHandlers {

0 commit comments

Comments
 (0)