@textbus/collaborate 2.0.0-beta.7 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundles/collaborate-cursor.d.ts +11 -11
- package/bundles/collaborate-cursor.js +120 -117
- package/bundles/collaborate.d.ts +12 -7
- package/bundles/collaborate.js +264 -223
- package/bundles/public-api.d.ts +2 -0
- package/bundles/public-api.js +14 -1
- package/bundles/unknown.component.d.ts +1 -0
- package/bundles/unknown.component.js +22 -0
- package/package.json +7 -7
- package/src/collaborate-cursor.ts +100 -56
- package/src/collaborate.ts +261 -81
- package/src/public-api.ts +16 -0
- package/src/unknown.component.ts +22 -0
- package/tsconfig-build.json +1 -1
package/src/collaborate.ts
CHANGED
|
@@ -1,42 +1,100 @@
|
|
|
1
|
-
import { Injectable } from '@tanbo/di'
|
|
2
|
-
import {
|
|
1
|
+
import { Inject, Injectable } from '@tanbo/di'
|
|
2
|
+
import { delay, filter, map, Observable, Subject, Subscription } from '@tanbo/stream'
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
ChangeOrigin,
|
|
5
|
+
ComponentInstance,
|
|
6
|
+
ContentType, Controller,
|
|
7
|
+
Formats,
|
|
8
|
+
History, HISTORY_STACK_SIZE,
|
|
9
|
+
makeError,
|
|
7
10
|
Registry,
|
|
11
|
+
RootComponentRef,
|
|
12
|
+
Scheduler,
|
|
8
13
|
Selection,
|
|
9
14
|
SelectionPaths,
|
|
10
|
-
|
|
15
|
+
Slot,
|
|
16
|
+
Starter,
|
|
17
|
+
Translator
|
|
11
18
|
} from '@textbus/core'
|
|
12
19
|
import {
|
|
20
|
+
Array as YArray,
|
|
13
21
|
Doc as YDoc,
|
|
14
22
|
Map as YMap,
|
|
23
|
+
RelativePosition,
|
|
15
24
|
Text as YText,
|
|
16
|
-
|
|
25
|
+
Transaction,
|
|
17
26
|
UndoManager,
|
|
18
|
-
|
|
27
|
+
createAbsolutePositionFromRelativePosition,
|
|
28
|
+
createRelativePositionFromTypeIndex
|
|
19
29
|
} from 'yjs'
|
|
20
30
|
|
|
21
31
|
import { CollaborateCursor, RemoteSelection } from './collaborate-cursor'
|
|
32
|
+
import { createUnknownComponent } from './unknown.component'
|
|
22
33
|
|
|
23
34
|
const collaborateErrorFn = makeError('Collaborate')
|
|
24
35
|
|
|
36
|
+
interface CursorPosition {
|
|
37
|
+
anchor: RelativePosition
|
|
38
|
+
focus: RelativePosition
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class ContentMap {
|
|
42
|
+
private slotAndYTextMap = new WeakMap<Slot, YText>()
|
|
43
|
+
private yTextAndSLotMap = new WeakMap<YText, Slot>()
|
|
44
|
+
|
|
45
|
+
set(key: Slot, value: YText): void
|
|
46
|
+
set(key: YText, value: Slot): void
|
|
47
|
+
set(key: any, value: any) {
|
|
48
|
+
if (key instanceof Slot) {
|
|
49
|
+
this.slotAndYTextMap.set(key, value)
|
|
50
|
+
this.yTextAndSLotMap.set(value, key)
|
|
51
|
+
} else {
|
|
52
|
+
this.slotAndYTextMap.set(value, key)
|
|
53
|
+
this.yTextAndSLotMap.set(key, value)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get(key: Slot): YText | null
|
|
58
|
+
get(key: YText): Slot | null
|
|
59
|
+
get(key: any) {
|
|
60
|
+
if (key instanceof Slot) {
|
|
61
|
+
return this.slotAndYTextMap.get(key) || null
|
|
62
|
+
}
|
|
63
|
+
return this.yTextAndSLotMap.get(key) || null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
delete(key: Slot | YText) {
|
|
67
|
+
if (key instanceof Slot) {
|
|
68
|
+
const v = this.slotAndYTextMap.get(key)
|
|
69
|
+
this.slotAndYTextMap.delete(key)
|
|
70
|
+
if (v) {
|
|
71
|
+
this.yTextAndSLotMap.delete(v)
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
const v = this.yTextAndSLotMap.get(key)
|
|
75
|
+
this.yTextAndSLotMap.delete(key)
|
|
76
|
+
if (v) {
|
|
77
|
+
this.slotAndYTextMap.delete(v)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
25
83
|
@Injectable()
|
|
26
84
|
export class Collaborate implements History {
|
|
27
85
|
onSelectionChange: Observable<SelectionPaths>
|
|
28
86
|
yDoc = new YDoc()
|
|
29
87
|
onBack: Observable<void>
|
|
30
88
|
onForward: Observable<void>
|
|
31
|
-
onChange: Observable<
|
|
89
|
+
onChange: Observable<void>
|
|
32
90
|
onPush: Observable<void>
|
|
33
91
|
|
|
34
92
|
get canBack() {
|
|
35
|
-
return this.manager?.canUndo()
|
|
93
|
+
return this.manager?.canUndo() || false
|
|
36
94
|
}
|
|
37
95
|
|
|
38
96
|
get canForward() {
|
|
39
|
-
return this.manager?.canRedo()
|
|
97
|
+
return this.manager?.canRedo() || false
|
|
40
98
|
}
|
|
41
99
|
|
|
42
100
|
private backEvent = new Subject<void>()
|
|
@@ -44,7 +102,7 @@ export class Collaborate implements History {
|
|
|
44
102
|
private changeEvent = new Subject<void>()
|
|
45
103
|
private pushEvent = new Subject<void>()
|
|
46
104
|
|
|
47
|
-
private manager
|
|
105
|
+
private manager: UndoManager | null = null
|
|
48
106
|
|
|
49
107
|
private subscriptions: Subscription[] = []
|
|
50
108
|
private updateFromRemote = false
|
|
@@ -55,87 +113,179 @@ export class Collaborate implements History {
|
|
|
55
113
|
private componentStateSyncCaches = new WeakMap<ComponentInstance, () => void>()
|
|
56
114
|
|
|
57
115
|
private selectionChangeEvent = new Subject<SelectionPaths>()
|
|
116
|
+
private contentMap = new ContentMap()
|
|
58
117
|
|
|
59
118
|
private updateRemoteActions: Array<() => void> = []
|
|
60
119
|
|
|
61
|
-
constructor(private
|
|
120
|
+
constructor(@Inject(HISTORY_STACK_SIZE) private stackSize: number,
|
|
121
|
+
private rootComponentRef: RootComponentRef,
|
|
62
122
|
private collaborateCursor: CollaborateCursor,
|
|
123
|
+
private controller: Controller,
|
|
124
|
+
private scheduler: Scheduler,
|
|
63
125
|
private translator: Translator,
|
|
64
|
-
private renderer: Renderer,
|
|
65
126
|
private registry: Registry,
|
|
66
127
|
private selection: Selection,
|
|
67
128
|
private starter: Starter) {
|
|
68
|
-
this.onSelectionChange = this.selectionChangeEvent.asObservable()
|
|
129
|
+
this.onSelectionChange = this.selectionChangeEvent.asObservable().pipe(delay())
|
|
69
130
|
this.onBack = this.backEvent.asObservable()
|
|
70
131
|
this.onForward = this.forwardEvent.asObservable()
|
|
71
132
|
this.onChange = this.changeEvent.asObservable()
|
|
72
133
|
this.onPush = this.pushEvent.asObservable()
|
|
73
134
|
}
|
|
74
135
|
|
|
75
|
-
|
|
136
|
+
listen() {
|
|
137
|
+
const root = this.yDoc.getMap('RootComponent')
|
|
138
|
+
const rootComponent = this.rootComponentRef.component!
|
|
139
|
+
this.manager = new UndoManager(root, {
|
|
140
|
+
trackedOrigins: new Set<any>([this.yDoc])
|
|
141
|
+
})
|
|
142
|
+
const cursorKey = 'cursor-position'
|
|
143
|
+
this.manager.on('stack-item-added', event => {
|
|
144
|
+
event.stackItem.meta.set(cursorKey, this.getRelativeCursorLocation())
|
|
145
|
+
if (this.manager!.undoStack.length > this.stackSize) {
|
|
146
|
+
this.manager!.undoStack.shift()
|
|
147
|
+
}
|
|
148
|
+
if (event.origin === this.yDoc) {
|
|
149
|
+
this.pushEvent.next()
|
|
150
|
+
}
|
|
151
|
+
this.changeEvent.next()
|
|
152
|
+
})
|
|
153
|
+
this.manager.on('stack-item-popped', event => {
|
|
154
|
+
const position = event.stackItem.meta.get(cursorKey) as CursorPosition
|
|
155
|
+
if (position) {
|
|
156
|
+
this.restoreCursorLocation(position)
|
|
157
|
+
}
|
|
158
|
+
})
|
|
76
159
|
this.subscriptions.push(
|
|
77
|
-
this.starter.onReady.subscribe(() => {
|
|
78
|
-
this.listen2()
|
|
79
|
-
}),
|
|
80
160
|
this.selection.onChange.subscribe(() => {
|
|
81
161
|
const paths = this.selection.getPaths()
|
|
82
162
|
this.selectionChangeEvent.next(paths)
|
|
163
|
+
}),
|
|
164
|
+
this.scheduler.onDocChanged.pipe(
|
|
165
|
+
map(item => {
|
|
166
|
+
return item.filter(i => {
|
|
167
|
+
return i.from !== ChangeOrigin.Remote
|
|
168
|
+
})
|
|
169
|
+
}),
|
|
170
|
+
filter(item => {
|
|
171
|
+
return item.length
|
|
172
|
+
})
|
|
173
|
+
).subscribe(() => {
|
|
174
|
+
this.yDoc.transact(() => {
|
|
175
|
+
this.updateRemoteActions.forEach(fn => {
|
|
176
|
+
fn()
|
|
177
|
+
})
|
|
178
|
+
this.updateRemoteActions = []
|
|
179
|
+
}, this.yDoc)
|
|
83
180
|
})
|
|
84
181
|
)
|
|
182
|
+
this.syncRootComponent(root, rootComponent)
|
|
85
183
|
}
|
|
86
184
|
|
|
87
185
|
updateRemoteSelection(paths: RemoteSelection[]) {
|
|
88
186
|
this.collaborateCursor.draw(paths)
|
|
89
187
|
}
|
|
90
188
|
|
|
91
|
-
listen() {
|
|
92
|
-
//
|
|
93
|
-
}
|
|
94
|
-
|
|
95
189
|
back() {
|
|
96
190
|
if (this.canBack) {
|
|
97
|
-
this.manager
|
|
191
|
+
this.manager?.undo()
|
|
192
|
+
this.backEvent.next()
|
|
98
193
|
}
|
|
99
194
|
}
|
|
100
195
|
|
|
101
196
|
forward() {
|
|
102
197
|
if (this.canForward) {
|
|
103
|
-
this.manager
|
|
198
|
+
this.manager?.redo()
|
|
199
|
+
this.forwardEvent.next()
|
|
104
200
|
}
|
|
105
201
|
}
|
|
106
202
|
|
|
203
|
+
clear() {
|
|
204
|
+
this.manager?.clear()
|
|
205
|
+
this.changeEvent.next()
|
|
206
|
+
}
|
|
207
|
+
|
|
107
208
|
destroy() {
|
|
108
209
|
this.subscriptions.forEach(i => i.unsubscribe())
|
|
210
|
+
this.collaborateCursor.destroy()
|
|
211
|
+
this.manager?.destroy()
|
|
109
212
|
}
|
|
110
213
|
|
|
111
|
-
private
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
this.subscriptions.push(
|
|
120
|
-
merge(
|
|
121
|
-
rootComponent.changeMarker.onForceChange,
|
|
122
|
-
rootComponent.changeMarker.onChange
|
|
123
|
-
).pipe(
|
|
124
|
-
microTask()
|
|
125
|
-
).subscribe(() => {
|
|
126
|
-
this.yDoc.transact(() => {
|
|
127
|
-
this.updateRemoteActions.forEach(fn => {
|
|
128
|
-
fn()
|
|
129
|
-
})
|
|
130
|
-
this.updateRemoteActions = []
|
|
131
|
-
}, this.yDoc)
|
|
132
|
-
this.renderer.render()
|
|
133
|
-
this.selection.restore()
|
|
214
|
+
private syncRootComponent(root: YMap<any>, rootComponent: ComponentInstance) {
|
|
215
|
+
let slots = root.get('slots') as YArray<YMap<any>>
|
|
216
|
+
if (!slots) {
|
|
217
|
+
slots = new YArray()
|
|
218
|
+
rootComponent.slots.toArray().forEach(i => {
|
|
219
|
+
const sharedSlot = this.createSharedSlotBySlot(i)
|
|
220
|
+
slots.push([sharedSlot])
|
|
134
221
|
})
|
|
135
|
-
|
|
222
|
+
this.yDoc.transact(() => {
|
|
223
|
+
root.set('state', rootComponent.state)
|
|
224
|
+
root.set('slots', slots)
|
|
225
|
+
})
|
|
226
|
+
} else if (slots.length === 0) {
|
|
227
|
+
rootComponent.updateState(() => {
|
|
228
|
+
return root.get('state')
|
|
229
|
+
})
|
|
230
|
+
this.yDoc.transact(() => {
|
|
231
|
+
rootComponent.slots.toArray().forEach(i => {
|
|
232
|
+
const sharedSlot = this.createSharedSlotBySlot(i)
|
|
233
|
+
slots.push([sharedSlot])
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
} else {
|
|
237
|
+
rootComponent.updateState(() => {
|
|
238
|
+
return root.get('state')
|
|
239
|
+
})
|
|
240
|
+
rootComponent.slots.clean()
|
|
241
|
+
slots.forEach(sharedSlot => {
|
|
242
|
+
const slot = this.createSlotBySharedSlot(sharedSlot)
|
|
243
|
+
this.syncContent(sharedSlot.get('content'), slot)
|
|
244
|
+
this.syncSlot(sharedSlot, slot)
|
|
245
|
+
rootComponent.slots.insert(slot)
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
this.syncComponent(root, rootComponent)
|
|
249
|
+
this.syncSlots(slots, rootComponent)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private restoreCursorLocation(position: CursorPosition) {
|
|
253
|
+
const anchorPosition = createAbsolutePositionFromRelativePosition(position.anchor, this.yDoc)
|
|
254
|
+
const focusPosition = createAbsolutePositionFromRelativePosition(position.focus, this.yDoc)
|
|
255
|
+
if (anchorPosition && focusPosition) {
|
|
256
|
+
const focusSlot = this.contentMap.get(focusPosition.type as YText)
|
|
257
|
+
const anchorSlot = this.contentMap.get(anchorPosition.type as YText)
|
|
258
|
+
if (focusSlot && anchorSlot) {
|
|
259
|
+
this.selection.setBaseAndExtent(anchorSlot, anchorPosition.index, focusSlot, focusPosition.index)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
this.selection.unSelect()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private getRelativeCursorLocation(): CursorPosition | null {
|
|
267
|
+
const { anchorSlot, anchorOffset, focusSlot, focusOffset } = this.selection
|
|
268
|
+
if (anchorSlot) {
|
|
269
|
+
const anchorYText = this.contentMap.get(anchorSlot)
|
|
270
|
+
if (anchorYText) {
|
|
271
|
+
const anchorPosition = createRelativePositionFromTypeIndex(anchorYText, anchorOffset!)
|
|
272
|
+
if (focusSlot) {
|
|
273
|
+
const focusYText = this.contentMap.get(focusSlot)
|
|
274
|
+
if (focusYText) {
|
|
275
|
+
const focusPosition = createRelativePositionFromTypeIndex(focusYText, focusOffset!)
|
|
276
|
+
return {
|
|
277
|
+
focus: focusPosition,
|
|
278
|
+
anchor: anchorPosition
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return null
|
|
136
285
|
}
|
|
137
286
|
|
|
138
287
|
private syncContent(content: YText, slot: Slot) {
|
|
288
|
+
this.contentMap.set(slot, content)
|
|
139
289
|
const syncRemote = (ev, tr) => {
|
|
140
290
|
this.runRemoteUpdate(tr, () => {
|
|
141
291
|
slot.retain(0)
|
|
@@ -146,6 +296,7 @@ export class Collaborate implements History {
|
|
|
146
296
|
if (formats.length) {
|
|
147
297
|
slot.retain(action.retain!, formats)
|
|
148
298
|
}
|
|
299
|
+
slot.retain(slot.index + action.retain)
|
|
149
300
|
} else {
|
|
150
301
|
slot.retain(action.retain)
|
|
151
302
|
}
|
|
@@ -157,17 +308,18 @@ export class Collaborate implements History {
|
|
|
157
308
|
slot.insert(action.insert, makeFormats(this.registry, action.attributes))
|
|
158
309
|
} else {
|
|
159
310
|
const sharedComponent = action.insert as YMap<any>
|
|
160
|
-
const
|
|
311
|
+
const canInsertInlineComponent = slot.schema.includes(ContentType.InlineComponent)
|
|
312
|
+
const component = this.createComponentBySharedComponent(sharedComponent, canInsertInlineComponent)
|
|
161
313
|
this.syncSlots(sharedComponent.get('slots'), component)
|
|
162
314
|
this.syncComponent(sharedComponent, component)
|
|
163
315
|
slot.insert(component)
|
|
164
316
|
}
|
|
165
317
|
if (this.selection.isSelected) {
|
|
166
|
-
if (slot === this.selection.
|
|
167
|
-
this.selection.
|
|
318
|
+
if (slot === this.selection.anchorSlot && this.selection.anchorOffset! > index) {
|
|
319
|
+
this.selection.setAnchor(slot, this.selection.anchorOffset! + length)
|
|
168
320
|
}
|
|
169
|
-
if (slot === this.selection.
|
|
170
|
-
this.selection.
|
|
321
|
+
if (slot === this.selection.focusSlot && this.selection.focusOffset! > index) {
|
|
322
|
+
this.selection.setFocus(slot, this.selection.focusOffset! + length)
|
|
171
323
|
}
|
|
172
324
|
}
|
|
173
325
|
} else if (action.delete) {
|
|
@@ -175,16 +327,20 @@ export class Collaborate implements History {
|
|
|
175
327
|
slot.retain(slot.index)
|
|
176
328
|
slot.delete(action.delete)
|
|
177
329
|
if (this.selection.isSelected) {
|
|
178
|
-
if (slot === this.selection.
|
|
179
|
-
this.selection.
|
|
330
|
+
if (slot === this.selection.anchorSlot && this.selection.anchorOffset! >= index) {
|
|
331
|
+
this.selection.setAnchor(slot, this.selection.startOffset! - action.delete)
|
|
180
332
|
}
|
|
181
|
-
if (slot === this.selection.
|
|
182
|
-
this.selection.
|
|
333
|
+
if (slot === this.selection.focusSlot && this.selection.focusOffset! >= index) {
|
|
334
|
+
this.selection.setFocus(slot, this.selection.focusOffset! - action.delete)
|
|
183
335
|
}
|
|
184
336
|
}
|
|
185
337
|
} else if (action.attributes) {
|
|
186
338
|
slot.updateState(draft => {
|
|
187
|
-
|
|
339
|
+
if (typeof draft === 'object' && draft !== null) {
|
|
340
|
+
Object.assign(draft, action.attributes)
|
|
341
|
+
} else {
|
|
342
|
+
return action.attributes
|
|
343
|
+
}
|
|
188
344
|
})
|
|
189
345
|
}
|
|
190
346
|
})
|
|
@@ -219,23 +375,22 @@ export class Collaborate implements History {
|
|
|
219
375
|
const isEmpty = delta.length === 1 && delta[0].insert === Slot.emptyPlaceholder
|
|
220
376
|
if (typeof action.content === 'string') {
|
|
221
377
|
length = action.content.length
|
|
222
|
-
content.insert(offset, action.content)
|
|
378
|
+
content.insert(offset, action.content, action.formats || {})
|
|
223
379
|
} else {
|
|
224
380
|
length = 1
|
|
225
|
-
const
|
|
226
|
-
const sharedComponent = this.createSharedComponentByComponent(component)
|
|
381
|
+
const sharedComponent = this.createSharedComponentByComponent(action.ref as ComponentInstance)
|
|
227
382
|
content.insertEmbed(offset, sharedComponent)
|
|
228
383
|
}
|
|
229
|
-
|
|
230
|
-
content.format(offset, length, action.formats)
|
|
231
|
-
}
|
|
384
|
+
|
|
232
385
|
if (isEmpty && offset === 0) {
|
|
233
386
|
content.delete(content.length - 1, 1)
|
|
234
387
|
}
|
|
235
388
|
offset += length
|
|
236
389
|
} else if (action.type === 'delete') {
|
|
237
390
|
const delta = content.toDelta()
|
|
238
|
-
content.
|
|
391
|
+
if (content.length) {
|
|
392
|
+
content.delete(offset, action.count)
|
|
393
|
+
}
|
|
239
394
|
if (content.length === 0) {
|
|
240
395
|
content.insert(0, '\n', delta[0]?.attributes)
|
|
241
396
|
}
|
|
@@ -243,6 +398,12 @@ export class Collaborate implements History {
|
|
|
243
398
|
}
|
|
244
399
|
})
|
|
245
400
|
})
|
|
401
|
+
|
|
402
|
+
sub.add(slot.onChildComponentRemove.subscribe(components => {
|
|
403
|
+
components.forEach(c => {
|
|
404
|
+
this.cleanSubscriptionsByComponent(c)
|
|
405
|
+
})
|
|
406
|
+
}))
|
|
246
407
|
this.contentSyncCaches.set(slot, () => {
|
|
247
408
|
content.unobserve(syncRemote)
|
|
248
409
|
sub.unsubscribe()
|
|
@@ -256,7 +417,11 @@ export class Collaborate implements History {
|
|
|
256
417
|
if (key === 'state') {
|
|
257
418
|
const state = (ev.target as YMap<any>).get('state')
|
|
258
419
|
slot.updateState(draft => {
|
|
259
|
-
|
|
420
|
+
if (typeof draft === 'object' && draft !== null) {
|
|
421
|
+
Object.assign(draft, state)
|
|
422
|
+
} else {
|
|
423
|
+
return state
|
|
424
|
+
}
|
|
260
425
|
})
|
|
261
426
|
}
|
|
262
427
|
})
|
|
@@ -281,18 +446,21 @@ export class Collaborate implements History {
|
|
|
281
446
|
const slots = component.slots
|
|
282
447
|
const syncRemote = (ev, tr) => {
|
|
283
448
|
this.runRemoteUpdate(tr, () => {
|
|
449
|
+
let index = 0
|
|
284
450
|
ev.delta.forEach(action => {
|
|
285
451
|
if (Reflect.has(action, 'retain')) {
|
|
286
|
-
|
|
452
|
+
index += action.retain
|
|
453
|
+
slots.retain(index)
|
|
287
454
|
} else if (action.insert) {
|
|
288
455
|
(action.insert as Array<YMap<any>>).forEach(item => {
|
|
289
456
|
const slot = this.createSlotBySharedSlot(item)
|
|
290
457
|
slots.insert(slot)
|
|
291
458
|
this.syncContent(item.get('content'), slot)
|
|
292
459
|
this.syncSlot(item, slot)
|
|
460
|
+
index++
|
|
293
461
|
})
|
|
294
462
|
} else if (action.delete) {
|
|
295
|
-
slots.retain(
|
|
463
|
+
slots.retain(index)
|
|
296
464
|
slots.delete(action.delete)
|
|
297
465
|
}
|
|
298
466
|
})
|
|
@@ -308,20 +476,22 @@ export class Collaborate implements History {
|
|
|
308
476
|
if (action.type === 'retain') {
|
|
309
477
|
index = action.offset
|
|
310
478
|
} else if (action.type === 'insertSlot') {
|
|
311
|
-
const
|
|
312
|
-
const sharedSlot = this.createSharedSlotBySlot(slot)
|
|
479
|
+
const sharedSlot = this.createSharedSlotBySlot(action.ref)
|
|
313
480
|
remoteSlots.insert(index, [sharedSlot])
|
|
314
481
|
index++
|
|
315
482
|
} else if (action.type === 'delete') {
|
|
316
|
-
slots.slice(index, index + action.count).forEach(slot => {
|
|
317
|
-
this.cleanSubscriptionsBySlot(slot)
|
|
318
|
-
})
|
|
319
483
|
remoteSlots.delete(index, action.count)
|
|
320
484
|
}
|
|
321
485
|
})
|
|
322
486
|
})
|
|
323
487
|
})
|
|
324
488
|
|
|
489
|
+
sub.add(slots.onChildSlotRemove.subscribe(slots => {
|
|
490
|
+
slots.forEach(slot => {
|
|
491
|
+
this.cleanSubscriptionsBySlot(slot)
|
|
492
|
+
})
|
|
493
|
+
}))
|
|
494
|
+
|
|
325
495
|
this.slotsSyncCaches.set(component, () => {
|
|
326
496
|
remoteSlots.unobserve(syncRemote)
|
|
327
497
|
sub.unsubscribe()
|
|
@@ -335,7 +505,11 @@ export class Collaborate implements History {
|
|
|
335
505
|
if (key === 'state') {
|
|
336
506
|
const state = (ev.target as YMap<any>).get('state')
|
|
337
507
|
component.updateState(draft => {
|
|
338
|
-
|
|
508
|
+
if (typeof draft === 'object' && draft !== null) {
|
|
509
|
+
Object.assign(draft, state)
|
|
510
|
+
} else {
|
|
511
|
+
return state
|
|
512
|
+
}
|
|
339
513
|
})
|
|
340
514
|
}
|
|
341
515
|
})
|
|
@@ -355,7 +529,7 @@ export class Collaborate implements History {
|
|
|
355
529
|
}
|
|
356
530
|
|
|
357
531
|
private runLocalUpdate(fn: () => void) {
|
|
358
|
-
if (this.updateFromRemote) {
|
|
532
|
+
if (this.updateFromRemote || this.controller.readonly) {
|
|
359
533
|
return
|
|
360
534
|
}
|
|
361
535
|
this.updateRemoteActions.push(fn)
|
|
@@ -366,7 +540,11 @@ export class Collaborate implements History {
|
|
|
366
540
|
return
|
|
367
541
|
}
|
|
368
542
|
this.updateFromRemote = true
|
|
369
|
-
|
|
543
|
+
if (tr.origin === this.manager) {
|
|
544
|
+
this.scheduler.historyApplyTransact(fn)
|
|
545
|
+
} else {
|
|
546
|
+
this.scheduler.remoteUpdateTransact(fn)
|
|
547
|
+
}
|
|
370
548
|
this.updateFromRemote = false
|
|
371
549
|
}
|
|
372
550
|
|
|
@@ -414,7 +592,7 @@ export class Collaborate implements History {
|
|
|
414
592
|
return sharedSlot
|
|
415
593
|
}
|
|
416
594
|
|
|
417
|
-
private createComponentBySharedComponent(yMap: YMap<any
|
|
595
|
+
private createComponentBySharedComponent(yMap: YMap<any>, canInsertInlineComponent: boolean): ComponentInstance {
|
|
418
596
|
const sharedSlots = yMap.get('slots') as YArray<YMap<any>>
|
|
419
597
|
const slots: Slot[] = []
|
|
420
598
|
sharedSlots.forEach(sharedSlot => {
|
|
@@ -438,7 +616,7 @@ export class Collaborate implements History {
|
|
|
438
616
|
})
|
|
439
617
|
return instance
|
|
440
618
|
}
|
|
441
|
-
|
|
619
|
+
return createUnknownComponent(name, canInsertInlineComponent).createInstance(this.starter)
|
|
442
620
|
}
|
|
443
621
|
|
|
444
622
|
private createSlotBySharedSlot(sharedSlot: YMap<any>): Slot {
|
|
@@ -458,7 +636,8 @@ export class Collaborate implements History {
|
|
|
458
636
|
slot.insert(action.insert, makeFormats(this.registry, action.attributes))
|
|
459
637
|
} else {
|
|
460
638
|
const sharedComponent = action.insert as YMap<any>
|
|
461
|
-
const
|
|
639
|
+
const canInsertInlineComponent = slot.schema.includes(ContentType.InlineComponent)
|
|
640
|
+
const component = this.createComponentBySharedComponent(sharedComponent, canInsertInlineComponent)
|
|
462
641
|
slot.insert(component)
|
|
463
642
|
this.syncSlots(sharedComponent.get('slots'), component)
|
|
464
643
|
this.syncComponent(sharedComponent, component)
|
|
@@ -471,6 +650,7 @@ export class Collaborate implements History {
|
|
|
471
650
|
}
|
|
472
651
|
|
|
473
652
|
private cleanSubscriptionsBySlot(slot: Slot) {
|
|
653
|
+
this.contentMap.delete(slot);
|
|
474
654
|
[this.contentSyncCaches.get(slot), this.slotStateSyncCaches.get(slot)].forEach(fn => {
|
|
475
655
|
if (fn) {
|
|
476
656
|
fn()
|
|
@@ -498,7 +678,7 @@ export class Collaborate implements History {
|
|
|
498
678
|
function makeFormats(registry: Registry, attrs?: any) {
|
|
499
679
|
const formats: Formats = []
|
|
500
680
|
if (attrs) {
|
|
501
|
-
Object.keys(attrs).
|
|
681
|
+
Object.keys(attrs).forEach(key => {
|
|
502
682
|
const formatter = registry.getFormatter(key)
|
|
503
683
|
if (formatter) {
|
|
504
684
|
formats.push([formatter, attrs[key]])
|
package/src/public-api.ts
CHANGED
|
@@ -1,2 +1,18 @@
|
|
|
1
|
+
import { History, Module } from '@textbus/core'
|
|
2
|
+
|
|
3
|
+
import { Collaborate } from './collaborate'
|
|
4
|
+
import { CollaborateCursor } from './collaborate-cursor'
|
|
5
|
+
|
|
1
6
|
export * from './collaborate'
|
|
2
7
|
export * from './collaborate-cursor'
|
|
8
|
+
|
|
9
|
+
export const collaborateModule: Module = {
|
|
10
|
+
providers: [
|
|
11
|
+
Collaborate,
|
|
12
|
+
CollaborateCursor,
|
|
13
|
+
{
|
|
14
|
+
provide: History,
|
|
15
|
+
useClass: Collaborate
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ContentType, defineComponent, VElement } from '@textbus/core'
|
|
2
|
+
|
|
3
|
+
export function createUnknownComponent(factoryName: string, canInsertInlineComponent: boolean) {
|
|
4
|
+
const unknownComponent = defineComponent({
|
|
5
|
+
type: canInsertInlineComponent ? ContentType.InlineComponent : ContentType.BlockComponent,
|
|
6
|
+
name: 'UnknownComponent',
|
|
7
|
+
setup() {
|
|
8
|
+
console.error(`cannot find component factory \`${factoryName}\`.`)
|
|
9
|
+
return {
|
|
10
|
+
render() {
|
|
11
|
+
return VElement.createElement('textbus-unknown-component', {
|
|
12
|
+
style: {
|
|
13
|
+
display: canInsertInlineComponent ? 'inline' : 'block',
|
|
14
|
+
color: '#f00'
|
|
15
|
+
}
|
|
16
|
+
}, unknownComponent.name)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
return unknownComponent
|
|
22
|
+
}
|