@textbus/collaborate 2.0.0-beta.9 → 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 +11 -6
- package/bundles/collaborate.js +242 -216
- package/package.json +7 -7
- package/src/collaborate-cursor.ts +100 -56
- package/src/collaborate.ts +235 -66
- package/tsconfig-build.json +1 -1
package/src/collaborate.ts
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
-
import { Injectable } from '@tanbo/di'
|
2
|
-
import { Observable, Subject, Subscription } from '@tanbo/stream'
|
1
|
+
import { Inject, Injectable } from '@tanbo/di'
|
2
|
+
import { delay, filter, map, Observable, Subject, Subscription } from '@tanbo/stream'
|
3
3
|
import {
|
4
|
+
ChangeOrigin,
|
4
5
|
ComponentInstance,
|
5
|
-
ContentType,
|
6
|
+
ContentType, Controller,
|
6
7
|
Formats,
|
7
|
-
History,
|
8
|
+
History, HISTORY_STACK_SIZE,
|
8
9
|
makeError,
|
9
10
|
Registry,
|
10
11
|
RootComponentRef,
|
@@ -15,28 +16,85 @@ import {
|
|
15
16
|
Starter,
|
16
17
|
Translator
|
17
18
|
} from '@textbus/core'
|
18
|
-
import {
|
19
|
+
import {
|
20
|
+
Array as YArray,
|
21
|
+
Doc as YDoc,
|
22
|
+
Map as YMap,
|
23
|
+
RelativePosition,
|
24
|
+
Text as YText,
|
25
|
+
Transaction,
|
26
|
+
UndoManager,
|
27
|
+
createAbsolutePositionFromRelativePosition,
|
28
|
+
createRelativePositionFromTypeIndex
|
29
|
+
} from 'yjs'
|
19
30
|
|
20
31
|
import { CollaborateCursor, RemoteSelection } from './collaborate-cursor'
|
21
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,84 +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,
|
63
124
|
private scheduler: Scheduler,
|
64
125
|
private translator: Translator,
|
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.
|
98
|
-
this.
|
99
|
-
this.scheduler.ignoreChanges = false
|
191
|
+
this.manager?.undo()
|
192
|
+
this.backEvent.next()
|
100
193
|
}
|
101
194
|
}
|
102
195
|
|
103
196
|
forward() {
|
104
197
|
if (this.canForward) {
|
105
|
-
this.
|
106
|
-
this.
|
107
|
-
this.scheduler.ignoreChanges = false
|
198
|
+
this.manager?.redo()
|
199
|
+
this.forwardEvent.next()
|
108
200
|
}
|
109
201
|
}
|
110
202
|
|
203
|
+
clear() {
|
204
|
+
this.manager?.clear()
|
205
|
+
this.changeEvent.next()
|
206
|
+
}
|
207
|
+
|
111
208
|
destroy() {
|
112
209
|
this.subscriptions.forEach(i => i.unsubscribe())
|
210
|
+
this.collaborateCursor.destroy()
|
211
|
+
this.manager?.destroy()
|
113
212
|
}
|
114
213
|
|
115
|
-
private
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
this.subscriptions.push(
|
124
|
-
this.scheduler.onDocChange.subscribe(() => {
|
125
|
-
this.yDoc.transact(() => {
|
126
|
-
this.updateRemoteActions.forEach(fn => {
|
127
|
-
fn()
|
128
|
-
})
|
129
|
-
this.updateRemoteActions = []
|
130
|
-
}, this.yDoc)
|
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])
|
131
221
|
})
|
132
|
-
|
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
|
133
285
|
}
|
134
286
|
|
135
287
|
private syncContent(content: YText, slot: Slot) {
|
288
|
+
this.contentMap.set(slot, content)
|
136
289
|
const syncRemote = (ev, tr) => {
|
137
290
|
this.runRemoteUpdate(tr, () => {
|
138
291
|
slot.retain(0)
|
@@ -143,6 +296,7 @@ export class Collaborate implements History {
|
|
143
296
|
if (formats.length) {
|
144
297
|
slot.retain(action.retain!, formats)
|
145
298
|
}
|
299
|
+
slot.retain(slot.index + action.retain)
|
146
300
|
} else {
|
147
301
|
slot.retain(action.retain)
|
148
302
|
}
|
@@ -161,11 +315,11 @@ export class Collaborate implements History {
|
|
161
315
|
slot.insert(component)
|
162
316
|
}
|
163
317
|
if (this.selection.isSelected) {
|
164
|
-
if (slot === this.selection.
|
165
|
-
this.selection.
|
318
|
+
if (slot === this.selection.anchorSlot && this.selection.anchorOffset! > index) {
|
319
|
+
this.selection.setAnchor(slot, this.selection.anchorOffset! + length)
|
166
320
|
}
|
167
|
-
if (slot === this.selection.
|
168
|
-
this.selection.
|
321
|
+
if (slot === this.selection.focusSlot && this.selection.focusOffset! > index) {
|
322
|
+
this.selection.setFocus(slot, this.selection.focusOffset! + length)
|
169
323
|
}
|
170
324
|
}
|
171
325
|
} else if (action.delete) {
|
@@ -173,16 +327,20 @@ export class Collaborate implements History {
|
|
173
327
|
slot.retain(slot.index)
|
174
328
|
slot.delete(action.delete)
|
175
329
|
if (this.selection.isSelected) {
|
176
|
-
if (slot === this.selection.
|
177
|
-
this.selection.
|
330
|
+
if (slot === this.selection.anchorSlot && this.selection.anchorOffset! >= index) {
|
331
|
+
this.selection.setAnchor(slot, this.selection.startOffset! - action.delete)
|
178
332
|
}
|
179
|
-
if (slot === this.selection.
|
180
|
-
this.selection.
|
333
|
+
if (slot === this.selection.focusSlot && this.selection.focusOffset! >= index) {
|
334
|
+
this.selection.setFocus(slot, this.selection.focusOffset! - action.delete)
|
181
335
|
}
|
182
336
|
}
|
183
337
|
} else if (action.attributes) {
|
184
338
|
slot.updateState(draft => {
|
185
|
-
|
339
|
+
if (typeof draft === 'object' && draft !== null) {
|
340
|
+
Object.assign(draft, action.attributes)
|
341
|
+
} else {
|
342
|
+
return action.attributes
|
343
|
+
}
|
186
344
|
})
|
187
345
|
}
|
188
346
|
})
|
@@ -217,23 +375,22 @@ export class Collaborate implements History {
|
|
217
375
|
const isEmpty = delta.length === 1 && delta[0].insert === Slot.emptyPlaceholder
|
218
376
|
if (typeof action.content === 'string') {
|
219
377
|
length = action.content.length
|
220
|
-
content.insert(offset, action.content)
|
378
|
+
content.insert(offset, action.content, action.formats || {})
|
221
379
|
} else {
|
222
380
|
length = 1
|
223
|
-
const
|
224
|
-
const sharedComponent = this.createSharedComponentByComponent(component)
|
381
|
+
const sharedComponent = this.createSharedComponentByComponent(action.ref as ComponentInstance)
|
225
382
|
content.insertEmbed(offset, sharedComponent)
|
226
383
|
}
|
227
|
-
|
228
|
-
content.format(offset, length, action.formats)
|
229
|
-
}
|
384
|
+
|
230
385
|
if (isEmpty && offset === 0) {
|
231
386
|
content.delete(content.length - 1, 1)
|
232
387
|
}
|
233
388
|
offset += length
|
234
389
|
} else if (action.type === 'delete') {
|
235
390
|
const delta = content.toDelta()
|
236
|
-
content.
|
391
|
+
if (content.length) {
|
392
|
+
content.delete(offset, action.count)
|
393
|
+
}
|
237
394
|
if (content.length === 0) {
|
238
395
|
content.insert(0, '\n', delta[0]?.attributes)
|
239
396
|
}
|
@@ -260,7 +417,11 @@ export class Collaborate implements History {
|
|
260
417
|
if (key === 'state') {
|
261
418
|
const state = (ev.target as YMap<any>).get('state')
|
262
419
|
slot.updateState(draft => {
|
263
|
-
|
420
|
+
if (typeof draft === 'object' && draft !== null) {
|
421
|
+
Object.assign(draft, state)
|
422
|
+
} else {
|
423
|
+
return state
|
424
|
+
}
|
264
425
|
})
|
265
426
|
}
|
266
427
|
})
|
@@ -288,8 +449,8 @@ export class Collaborate implements History {
|
|
288
449
|
let index = 0
|
289
450
|
ev.delta.forEach(action => {
|
290
451
|
if (Reflect.has(action, 'retain')) {
|
291
|
-
slots.retain(action.retain!)
|
292
452
|
index += action.retain
|
453
|
+
slots.retain(index)
|
293
454
|
} else if (action.insert) {
|
294
455
|
(action.insert as Array<YMap<any>>).forEach(item => {
|
295
456
|
const slot = this.createSlotBySharedSlot(item)
|
@@ -315,8 +476,7 @@ export class Collaborate implements History {
|
|
315
476
|
if (action.type === 'retain') {
|
316
477
|
index = action.offset
|
317
478
|
} else if (action.type === 'insertSlot') {
|
318
|
-
const
|
319
|
-
const sharedSlot = this.createSharedSlotBySlot(slot)
|
479
|
+
const sharedSlot = this.createSharedSlotBySlot(action.ref)
|
320
480
|
remoteSlots.insert(index, [sharedSlot])
|
321
481
|
index++
|
322
482
|
} else if (action.type === 'delete') {
|
@@ -345,7 +505,11 @@ export class Collaborate implements History {
|
|
345
505
|
if (key === 'state') {
|
346
506
|
const state = (ev.target as YMap<any>).get('state')
|
347
507
|
component.updateState(draft => {
|
348
|
-
|
508
|
+
if (typeof draft === 'object' && draft !== null) {
|
509
|
+
Object.assign(draft, state)
|
510
|
+
} else {
|
511
|
+
return state
|
512
|
+
}
|
349
513
|
})
|
350
514
|
}
|
351
515
|
})
|
@@ -365,7 +529,7 @@ export class Collaborate implements History {
|
|
365
529
|
}
|
366
530
|
|
367
531
|
private runLocalUpdate(fn: () => void) {
|
368
|
-
if (this.updateFromRemote) {
|
532
|
+
if (this.updateFromRemote || this.controller.readonly) {
|
369
533
|
return
|
370
534
|
}
|
371
535
|
this.updateRemoteActions.push(fn)
|
@@ -376,7 +540,11 @@ export class Collaborate implements History {
|
|
376
540
|
return
|
377
541
|
}
|
378
542
|
this.updateFromRemote = true
|
379
|
-
|
543
|
+
if (tr.origin === this.manager) {
|
544
|
+
this.scheduler.historyApplyTransact(fn)
|
545
|
+
} else {
|
546
|
+
this.scheduler.remoteUpdateTransact(fn)
|
547
|
+
}
|
380
548
|
this.updateFromRemote = false
|
381
549
|
}
|
382
550
|
|
@@ -482,6 +650,7 @@ export class Collaborate implements History {
|
|
482
650
|
}
|
483
651
|
|
484
652
|
private cleanSubscriptionsBySlot(slot: Slot) {
|
653
|
+
this.contentMap.delete(slot);
|
485
654
|
[this.contentSyncCaches.get(slot), this.slotStateSyncCaches.get(slot)].forEach(fn => {
|
486
655
|
if (fn) {
|
487
656
|
fn()
|
@@ -509,7 +678,7 @@ export class Collaborate implements History {
|
|
509
678
|
function makeFormats(registry: Registry, attrs?: any) {
|
510
679
|
const formats: Formats = []
|
511
680
|
if (attrs) {
|
512
|
-
Object.keys(attrs).
|
681
|
+
Object.keys(attrs).forEach(key => {
|
513
682
|
const formatter = registry.getFormatter(key)
|
514
683
|
if (formatter) {
|
515
684
|
formats.push([formatter, attrs[key]])
|