@textbus/collaborate 2.0.0-beta.8 → 2.0.1

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.
@@ -1,42 +1,104 @@
1
- import { Injectable } from '@tanbo/di'
2
- import { microTask, Observable, Subject, Subscription } from '@tanbo/stream'
1
+ import { Inject, Injectable, Optional } from '@tanbo/di'
2
+ import { delay, filter, map, Observable, Subject, Subscription } from '@tanbo/stream'
3
3
  import {
4
+ ChangeOrigin, ComponentInitData,
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
- Renderer,
11
11
  RootComponentRef,
12
+ Scheduler,
12
13
  Selection,
13
14
  SelectionPaths,
14
15
  Slot,
15
16
  Starter,
16
17
  Translator
17
18
  } from '@textbus/core'
18
- import { Array as YArray, Doc as YDoc, Map as YMap, Text as YText, Transaction, UndoManager } from 'yjs'
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
+
83
+ export abstract class TranslatorFallback {
84
+ abstract createComponentByData(name: string, data: ComponentInitData): ComponentInstance | null
85
+ }
86
+
25
87
  @Injectable()
26
88
  export class Collaborate implements History {
27
89
  onSelectionChange: Observable<SelectionPaths>
28
90
  yDoc = new YDoc()
29
91
  onBack: Observable<void>
30
92
  onForward: Observable<void>
31
- onChange: Observable<any>
93
+ onChange: Observable<void>
32
94
  onPush: Observable<void>
33
95
 
34
96
  get canBack() {
35
- return this.manager?.canUndo()
97
+ return this.manager?.canUndo() || false
36
98
  }
37
99
 
38
100
  get canForward() {
39
- return this.manager?.canRedo()
101
+ return this.manager?.canRedo() || false
40
102
  }
41
103
 
42
104
  private backEvent = new Subject<void>()
@@ -44,7 +106,7 @@ export class Collaborate implements History {
44
106
  private changeEvent = new Subject<void>()
45
107
  private pushEvent = new Subject<void>()
46
108
 
47
- private manager!: UndoManager
109
+ private manager: UndoManager | null = null
48
110
 
49
111
  private subscriptions: Subscription[] = []
50
112
  private updateFromRemote = false
@@ -55,89 +117,180 @@ export class Collaborate implements History {
55
117
  private componentStateSyncCaches = new WeakMap<ComponentInstance, () => void>()
56
118
 
57
119
  private selectionChangeEvent = new Subject<SelectionPaths>()
120
+ private contentMap = new ContentMap()
58
121
 
59
122
  private updateRemoteActions: Array<() => void> = []
60
123
 
61
- constructor(private rootComponentRef: RootComponentRef,
124
+ constructor(@Inject(HISTORY_STACK_SIZE) private stackSize: number,
125
+ private rootComponentRef: RootComponentRef,
62
126
  private collaborateCursor: CollaborateCursor,
127
+ private controller: Controller,
128
+ private scheduler: Scheduler,
63
129
  private translator: Translator,
64
- private renderer: Renderer,
65
130
  private registry: Registry,
66
131
  private selection: Selection,
67
- private starter: Starter) {
68
- this.onSelectionChange = this.selectionChangeEvent.asObservable()
132
+ private starter: Starter,
133
+ @Optional() private translatorFallback: TranslatorFallback) {
134
+ this.onSelectionChange = this.selectionChangeEvent.asObservable().pipe(delay())
69
135
  this.onBack = this.backEvent.asObservable()
70
136
  this.onForward = this.forwardEvent.asObservable()
71
137
  this.onChange = this.changeEvent.asObservable()
72
138
  this.onPush = this.pushEvent.asObservable()
73
139
  }
74
140
 
75
- setup() {
141
+ listen() {
142
+ const root = this.yDoc.getMap('RootComponent')
143
+ const rootComponent = this.rootComponentRef.component!
144
+ this.manager = new UndoManager(root, {
145
+ trackedOrigins: new Set<any>([this.yDoc])
146
+ })
147
+ const cursorKey = 'cursor-position'
148
+ this.manager.on('stack-item-added', event => {
149
+ event.stackItem.meta.set(cursorKey, this.getRelativeCursorLocation())
150
+ if (this.manager!.undoStack.length > this.stackSize) {
151
+ this.manager!.undoStack.shift()
152
+ }
153
+ if (event.origin === this.yDoc) {
154
+ this.pushEvent.next()
155
+ }
156
+ this.changeEvent.next()
157
+ })
158
+ this.manager.on('stack-item-popped', event => {
159
+ const position = event.stackItem.meta.get(cursorKey) as CursorPosition
160
+ if (position) {
161
+ this.restoreCursorLocation(position)
162
+ }
163
+ })
76
164
  this.subscriptions.push(
77
- this.starter.onReady.subscribe(() => {
78
- this.listen2()
79
- }),
80
165
  this.selection.onChange.subscribe(() => {
81
166
  const paths = this.selection.getPaths()
82
167
  this.selectionChangeEvent.next(paths)
168
+ }),
169
+ this.scheduler.onDocChanged.pipe(
170
+ map(item => {
171
+ return item.filter(i => {
172
+ return i.from !== ChangeOrigin.Remote
173
+ })
174
+ }),
175
+ filter(item => {
176
+ return item.length
177
+ })
178
+ ).subscribe(() => {
179
+ this.yDoc.transact(() => {
180
+ this.updateRemoteActions.forEach(fn => {
181
+ fn()
182
+ })
183
+ this.updateRemoteActions = []
184
+ }, this.yDoc)
83
185
  })
84
186
  )
187
+ this.syncRootComponent(root, rootComponent)
85
188
  }
86
189
 
87
190
  updateRemoteSelection(paths: RemoteSelection[]) {
88
191
  this.collaborateCursor.draw(paths)
89
192
  }
90
193
 
91
- listen() {
92
- //
93
- }
94
-
95
194
  back() {
96
195
  if (this.canBack) {
97
- this.manager.undo()
196
+ this.manager?.undo()
197
+ this.backEvent.next()
98
198
  }
99
199
  }
100
200
 
101
201
  forward() {
102
202
  if (this.canForward) {
103
- this.manager.redo()
203
+ this.manager?.redo()
204
+ this.forwardEvent.next()
104
205
  }
105
206
  }
106
207
 
208
+ clear() {
209
+ this.manager?.clear()
210
+ this.changeEvent.next()
211
+ }
212
+
107
213
  destroy() {
108
214
  this.subscriptions.forEach(i => i.unsubscribe())
215
+ this.collaborateCursor.destroy()
216
+ this.manager?.destroy()
109
217
  }
110
218
 
111
- private listen2() {
112
- const root = this.yDoc.getText('content')
113
- const rootComponent = this.rootComponentRef.component!
114
- this.manager = new UndoManager(root, {
115
- trackedOrigins: new Set<any>([this.yDoc])
116
- })
117
- this.syncContent(root, rootComponent.slots.get(0)!)
118
-
119
- this.subscriptions.push(
120
- rootComponent.changeMarker.onForceChange.pipe(
121
- microTask()
122
- ).subscribe(() => {
123
- this.renderer.render()
124
- }),
125
- rootComponent.changeMarker.onChange.pipe(
126
- microTask()
127
- ).subscribe(() => {
128
- this.yDoc.transact(() => {
129
- this.updateRemoteActions.forEach(fn => {
130
- fn()
131
- })
132
- this.updateRemoteActions = []
133
- }, this.yDoc)
134
- this.renderer.render()
135
- this.selection.restore()
219
+ private syncRootComponent(root: YMap<any>, rootComponent: ComponentInstance) {
220
+ let slots = root.get('slots') as YArray<YMap<any>>
221
+ if (!slots) {
222
+ slots = new YArray()
223
+ rootComponent.slots.toArray().forEach(i => {
224
+ const sharedSlot = this.createSharedSlotBySlot(i)
225
+ slots.push([sharedSlot])
136
226
  })
137
- )
227
+ this.yDoc.transact(() => {
228
+ root.set('state', rootComponent.state)
229
+ root.set('slots', slots)
230
+ })
231
+ } else if (slots.length === 0) {
232
+ rootComponent.updateState(() => {
233
+ return root.get('state')
234
+ })
235
+ this.yDoc.transact(() => {
236
+ rootComponent.slots.toArray().forEach(i => {
237
+ const sharedSlot = this.createSharedSlotBySlot(i)
238
+ slots.push([sharedSlot])
239
+ })
240
+ })
241
+ } else {
242
+ rootComponent.updateState(() => {
243
+ return root.get('state')
244
+ })
245
+ rootComponent.slots.clean()
246
+ slots.forEach(sharedSlot => {
247
+ const slot = this.createSlotBySharedSlot(sharedSlot)
248
+ this.syncContent(sharedSlot.get('content'), slot)
249
+ this.syncSlot(sharedSlot, slot)
250
+ rootComponent.slots.insert(slot)
251
+ })
252
+ }
253
+ this.syncComponent(root, rootComponent)
254
+ this.syncSlots(slots, rootComponent)
255
+ }
256
+
257
+ private restoreCursorLocation(position: CursorPosition) {
258
+ const anchorPosition = createAbsolutePositionFromRelativePosition(position.anchor, this.yDoc)
259
+ const focusPosition = createAbsolutePositionFromRelativePosition(position.focus, this.yDoc)
260
+ if (anchorPosition && focusPosition) {
261
+ const focusSlot = this.contentMap.get(focusPosition.type as YText)
262
+ const anchorSlot = this.contentMap.get(anchorPosition.type as YText)
263
+ if (focusSlot && anchorSlot) {
264
+ this.selection.setBaseAndExtent(anchorSlot, anchorPosition.index, focusSlot, focusPosition.index)
265
+ return
266
+ }
267
+ }
268
+ this.selection.unSelect()
269
+ }
270
+
271
+ private getRelativeCursorLocation(): CursorPosition | null {
272
+ const { anchorSlot, anchorOffset, focusSlot, focusOffset } = this.selection
273
+ if (anchorSlot) {
274
+ const anchorYText = this.contentMap.get(anchorSlot)
275
+ if (anchorYText) {
276
+ const anchorPosition = createRelativePositionFromTypeIndex(anchorYText, anchorOffset!)
277
+ if (focusSlot) {
278
+ const focusYText = this.contentMap.get(focusSlot)
279
+ if (focusYText) {
280
+ const focusPosition = createRelativePositionFromTypeIndex(focusYText, focusOffset!)
281
+ return {
282
+ focus: focusPosition,
283
+ anchor: anchorPosition
284
+ }
285
+ }
286
+ }
287
+ }
288
+ }
289
+ return null
138
290
  }
139
291
 
140
292
  private syncContent(content: YText, slot: Slot) {
293
+ this.contentMap.set(slot, content)
141
294
  const syncRemote = (ev, tr) => {
142
295
  this.runRemoteUpdate(tr, () => {
143
296
  slot.retain(0)
@@ -148,6 +301,7 @@ export class Collaborate implements History {
148
301
  if (formats.length) {
149
302
  slot.retain(action.retain!, formats)
150
303
  }
304
+ slot.retain(slot.index + action.retain)
151
305
  } else {
152
306
  slot.retain(action.retain)
153
307
  }
@@ -166,11 +320,11 @@ export class Collaborate implements History {
166
320
  slot.insert(component)
167
321
  }
168
322
  if (this.selection.isSelected) {
169
- if (slot === this.selection.startSlot && this.selection.startOffset! >= index) {
170
- this.selection.setStart(slot, this.selection.startOffset! + length)
323
+ if (slot === this.selection.anchorSlot && this.selection.anchorOffset! > index) {
324
+ this.selection.setAnchor(slot, this.selection.anchorOffset! + length)
171
325
  }
172
- if (slot === this.selection.endSlot && this.selection.endOffset! >= index) {
173
- this.selection.setEnd(slot, this.selection.endOffset! + length)
326
+ if (slot === this.selection.focusSlot && this.selection.focusOffset! > index) {
327
+ this.selection.setFocus(slot, this.selection.focusOffset! + length)
174
328
  }
175
329
  }
176
330
  } else if (action.delete) {
@@ -178,16 +332,20 @@ export class Collaborate implements History {
178
332
  slot.retain(slot.index)
179
333
  slot.delete(action.delete)
180
334
  if (this.selection.isSelected) {
181
- if (slot === this.selection.startSlot && this.selection.startOffset! >= index) {
182
- this.selection.setStart(slot, this.selection.startOffset! - action.delete)
335
+ if (slot === this.selection.anchorSlot && this.selection.anchorOffset! >= index) {
336
+ this.selection.setAnchor(slot, this.selection.startOffset! - action.delete)
183
337
  }
184
- if (slot === this.selection.endSlot && this.selection.endOffset! >= index) {
185
- this.selection.setEnd(slot, this.selection.endOffset! - action.delete)
338
+ if (slot === this.selection.focusSlot && this.selection.focusOffset! >= index) {
339
+ this.selection.setFocus(slot, this.selection.focusOffset! - action.delete)
186
340
  }
187
341
  }
188
342
  } else if (action.attributes) {
189
343
  slot.updateState(draft => {
190
- Object.assign(draft, action.attributes)
344
+ if (typeof draft === 'object' && draft !== null) {
345
+ Object.assign(draft, action.attributes)
346
+ } else {
347
+ return action.attributes
348
+ }
191
349
  })
192
350
  }
193
351
  })
@@ -222,23 +380,22 @@ export class Collaborate implements History {
222
380
  const isEmpty = delta.length === 1 && delta[0].insert === Slot.emptyPlaceholder
223
381
  if (typeof action.content === 'string') {
224
382
  length = action.content.length
225
- content.insert(offset, action.content)
383
+ content.insert(offset, action.content, action.formats || {})
226
384
  } else {
227
385
  length = 1
228
- const component = slot.getContentAtIndex(offset) as ComponentInstance
229
- const sharedComponent = this.createSharedComponentByComponent(component)
386
+ const sharedComponent = this.createSharedComponentByComponent(action.ref as ComponentInstance)
230
387
  content.insertEmbed(offset, sharedComponent)
231
388
  }
232
- if (action.formats) {
233
- content.format(offset, length, action.formats)
234
- }
389
+
235
390
  if (isEmpty && offset === 0) {
236
391
  content.delete(content.length - 1, 1)
237
392
  }
238
393
  offset += length
239
394
  } else if (action.type === 'delete') {
240
395
  const delta = content.toDelta()
241
- content.delete(offset, action.count)
396
+ if (content.length) {
397
+ content.delete(offset, action.count)
398
+ }
242
399
  if (content.length === 0) {
243
400
  content.insert(0, '\n', delta[0]?.attributes)
244
401
  }
@@ -265,7 +422,11 @@ export class Collaborate implements History {
265
422
  if (key === 'state') {
266
423
  const state = (ev.target as YMap<any>).get('state')
267
424
  slot.updateState(draft => {
268
- Object.assign(draft, state)
425
+ if (typeof draft === 'object' && draft !== null) {
426
+ Object.assign(draft, state)
427
+ } else {
428
+ return state
429
+ }
269
430
  })
270
431
  }
271
432
  })
@@ -293,8 +454,8 @@ export class Collaborate implements History {
293
454
  let index = 0
294
455
  ev.delta.forEach(action => {
295
456
  if (Reflect.has(action, 'retain')) {
296
- slots.retain(action.retain!)
297
457
  index += action.retain
458
+ slots.retain(index)
298
459
  } else if (action.insert) {
299
460
  (action.insert as Array<YMap<any>>).forEach(item => {
300
461
  const slot = this.createSlotBySharedSlot(item)
@@ -320,8 +481,7 @@ export class Collaborate implements History {
320
481
  if (action.type === 'retain') {
321
482
  index = action.offset
322
483
  } else if (action.type === 'insertSlot') {
323
- const slot = slots.get(index)!
324
- const sharedSlot = this.createSharedSlotBySlot(slot)
484
+ const sharedSlot = this.createSharedSlotBySlot(action.ref)
325
485
  remoteSlots.insert(index, [sharedSlot])
326
486
  index++
327
487
  } else if (action.type === 'delete') {
@@ -350,7 +510,11 @@ export class Collaborate implements History {
350
510
  if (key === 'state') {
351
511
  const state = (ev.target as YMap<any>).get('state')
352
512
  component.updateState(draft => {
353
- Object.assign(draft, state)
513
+ if (typeof draft === 'object' && draft !== null) {
514
+ Object.assign(draft, state)
515
+ } else {
516
+ return state
517
+ }
354
518
  })
355
519
  }
356
520
  })
@@ -370,7 +534,7 @@ export class Collaborate implements History {
370
534
  }
371
535
 
372
536
  private runLocalUpdate(fn: () => void) {
373
- if (this.updateFromRemote) {
537
+ if (this.updateFromRemote || this.controller.readonly) {
374
538
  return
375
539
  }
376
540
  this.updateRemoteActions.push(fn)
@@ -381,7 +545,11 @@ export class Collaborate implements History {
381
545
  return
382
546
  }
383
547
  this.updateFromRemote = true
384
- fn()
548
+ if (tr.origin === this.manager) {
549
+ this.scheduler.historyApplyTransact(fn)
550
+ } else {
551
+ this.scheduler.remoteUpdateTransact(fn)
552
+ }
385
553
  this.updateFromRemote = false
386
554
  }
387
555
 
@@ -437,10 +605,17 @@ export class Collaborate implements History {
437
605
  slots.push(slot)
438
606
  })
439
607
  const name = yMap.get('name')
440
- const instance = this.translator.createComponentByData(name, {
441
- state: yMap.get('state'),
608
+ const state = yMap.get('state')
609
+ let instance = this.translator.createComponentByData(name, {
610
+ state,
442
611
  slots
443
612
  })
613
+ if (!instance) {
614
+ instance = this.translatorFallback.createComponentByData(name, {
615
+ state,
616
+ slots
617
+ })
618
+ }
444
619
  if (instance) {
445
620
  instance.slots.toArray().forEach((slot, index) => {
446
621
  let sharedSlot = sharedSlots.get(index)
@@ -487,6 +662,7 @@ export class Collaborate implements History {
487
662
  }
488
663
 
489
664
  private cleanSubscriptionsBySlot(slot: Slot) {
665
+ this.contentMap.delete(slot);
490
666
  [this.contentSyncCaches.get(slot), this.slotStateSyncCaches.get(slot)].forEach(fn => {
491
667
  if (fn) {
492
668
  fn()
@@ -514,7 +690,7 @@ export class Collaborate implements History {
514
690
  function makeFormats(registry: Registry, attrs?: any) {
515
691
  const formats: Formats = []
516
692
  if (attrs) {
517
- Object.keys(attrs).map(key => {
693
+ Object.keys(attrs).forEach(key => {
518
694
  const formatter = registry.getFormatter(key)
519
695
  if (formatter) {
520
696
  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
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "declaration": true,
4
- "useDefineForClassFields": true,
4
+ "useDefineForClassFields": false,
5
5
  "emitDecoratorMetadata": true,
6
6
  "experimentalDecorators": true,
7
7
  "allowSyntheticDefaultImports": true,